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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jonatan Holmgren committed Feb 18, 2026 at 22:57 UTC ac1f12a9de4b79b176a08f524fecdb092ff00e74
4 files changed +145 -18
Documentation/config/alias.adoc
+42 -8
@@ -1,12 +1,46 @@
1 alias.*::
2 - Command aliases for the linkgit:git[1] command wrapper - e.g.
3 - after defining `alias.last = cat-file commit HEAD`, the invocation
4 - `git last` is equivalent to `git cat-file commit HEAD`. To avoid
5 - confusion and troubles with script usage, aliases that
6 - hide existing Git commands are ignored except for deprecated
7 - commands. Arguments are split by
8 - spaces, the usual shell quoting and escaping are supported.
9 - A quote pair or a backslash can be used to quote them.
2 +alias.*.command::
3 + Command aliases for the linkgit:git[1] command wrapper. Aliases
4 + can be defined using two syntaxes:
5 ++
6 +--
7 +1. Without a subsection, e.g., `[alias] co = checkout`. The alias
8 + name ("co" in this example) is
9 + limited to ASCII alphanumeric characters and `-`,
10 + and is matched case-insensitively.
11 +2. With a subsection, e.g., `[alias "co"] command = checkout`. The
12 + alias name can contain any characters (except for newlines and NUL bytes),
13 + including UTF-8, and is matched case-sensitively as raw bytes.
14 + You define the action of the alias in the `command`.
15 +--
16 ++
17 +Examples:
18 ++
19 +----
20 +# Without subsection (ASCII alphanumeric and dash only)
21 +[alias]
22 + co = checkout
23 + st = status
24 +
25 +# With subsection (allows any characters, including UTF-8)
26 +[alias "hämta"]
27 + command = fetch
28 +[alias "rätta till"]
29 + command = commit --amend
30 +----
31 ++
32 +With a Git alias defined, e.g.,
33 +
34 + $ git config --global alias.last "cat-file commit HEAD"
35 + # Which is equivalent to
36 + $ git config --global alias.last.command "cat-file commit HEAD"
37 +
38 +`git last` is equivalent to `git cat-file commit HEAD`. To avoid
39 +confusion and troubles with script usage, aliases that
40 +hide existing Git commands are ignored except for deprecated
41 +commands. Arguments are split by
42 +spaces, the usual shell quoting and escaping are supported.
43 +A quote pair or a backslash can be used to quote them.
44 +
45 Note that the first word of an alias does not necessarily have to be a
46 command. It can be a command-line option that will be passed into the
alias.c
+31 -7
@@ -13,28 +13,52 @@ struct config_alias_data {
13 struct string_list *list;
14 };
15
16 -static int config_alias_cb(const char *key, const char *value,
16 +static int config_alias_cb(const char *var, const char *value,
17 const struct config_context *ctx UNUSED, void *d)
18 {
19 struct config_alias_data *data = d;
20 - const char *p;
20 + const char *subsection, *key;
21 + size_t subsection_len;
22
22 - if (!skip_prefix(key, "alias.", &p))
23 + if (parse_config_key(var, "alias", &subsection, &subsection_len,
24 + &key) < 0)
25 + return 0;
26 +
27 + /*
28 + * Two config syntaxes:
29 + * - alias.name = value (without subsection, case-insensitive)
30 + * - [alias "name"]
31 + * command = value (with subsection, case-sensitive)
32 + */
33 + if (subsection && strcmp(key, "command"))
34 return 0;
35
36 if (data->alias) {
26 - if (!strcasecmp(p, data->alias)) {
37 + int match;
38 +
39 + if (subsection)
40 + match = (strlen(data->alias) == subsection_len &&
41 + !strncmp(data->alias, subsection,
42 + subsection_len));
43 + else
44 + match = !strcasecmp(data->alias, key);
45 +
46 + if (match) {
47 FREE_AND_NULL(data->v);
48 return git_config_string(&data->v,
29 - key, value);
49 + var, value);
50 }
51 } else if (data->list) {
52 struct string_list_item *item;
53
54 if (!value)
35 - return config_error_nonbool(key);
55 + return config_error_nonbool(var);
56
37 - item = string_list_append(data->list, p);
57 + if (subsection)
58 + item = string_list_append_nodup(data->list,
59 + xmemdupz(subsection, subsection_len));
60 + else
61 + item = string_list_append(data->list, key);
62 item->util = xstrdup(value);
63 }
64
help.c
+11 -3
@@ -21,6 +21,7 @@
21 #include "fsmonitor-ipc.h"
22 #include "repository.h"
23 #include "alias.h"
24 +#include "utf8.h"
25
26 #ifndef NO_CURL
27 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
109
110 for (i = 0; cmds[i].name; i++) {
111 if (cmds[i].category & mask) {
111 - size_t len = strlen(cmds[i].name);
112 + size_t len = utf8_strwidth(cmds[i].name);
113 printf(" %s ", cmds[i].name);
114 if (longest > len)
115 mput_char(' ', longest - len);
@@ -493,7 +494,7 @@ static void list_all_cmds_help_aliases(int longest)
494 string_list_sort(&alias_list);
495
496 for (i = 0; i < alias_list.nr; i++) {
496 - size_t len = strlen(alias_list.items[i].string);
497 + size_t len = utf8_strwidth(alias_list.items[i].string);
498 if (longest < len)
499 longest = len;
500 }
@@ -592,8 +593,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
593 /* Also use aliases for command lookup */
594 if (!parse_config_key(var, "alias", &subsection, &subsection_len,
595 &key)) {
595 - if (!subsection)
596 + if (subsection) {
597 + /* [alias "name"] command = value */
598 + if (!strcmp(key, "command"))
599 + add_cmdname(&cfg->aliases, subsection,
600 + subsection_len);
601 + } else {
602 + /* alias.name = value */
603 add_cmdname(&cfg->aliases, key, strlen(key));
604 + }
605 }
606
607 return 0;
t/t0014-alias.sh
+61
@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '
122 test_grep "alias.noval" error
123 '
124
125 +test_expect_success 'subsection syntax works' '
126 + test_config alias.testnew.command "!echo ran-subsection" &&
127 + git testnew >output &&
128 + test_grep "ran-subsection" output
129 +'
130 +
131 +test_expect_success 'subsection syntax only accepts command key' '
132 + test_config alias.invalid.notcommand value &&
133 + test_must_fail git invalid 2>error &&
134 + test_grep -i "not a git command" error
135 +'
136 +
137 +test_expect_success 'subsection syntax requires value for command' '
138 + test_when_finished "git config --remove-section alias.noval" &&
139 + cat >>.git/config <<-\EOF &&
140 + [alias "noval"]
141 + command
142 + EOF
143 + test_must_fail git noval 2>error &&
144 + test_grep "alias.noval.command" error
145 +'
146 +
147 +test_expect_success 'simple syntax is case-insensitive' '
148 + test_config alias.LegacyCase "!echo ran-legacy" &&
149 + git legacycase >output &&
150 + test_grep "ran-legacy" output
151 +'
152 +
153 +test_expect_success 'subsection syntax is case-sensitive' '
154 + test_config alias.SubCase.command "!echo ran-upper" &&
155 + test_config alias.subcase.command "!echo ran-lower" &&
156 + git SubCase >upper.out &&
157 + git subcase >lower.out &&
158 + test_grep "ran-upper" upper.out &&
159 + test_grep "ran-lower" lower.out
160 +'
161 +
162 +test_expect_success 'UTF-8 alias with Swedish characters' '
163 + test_config alias."förgrena".command "!echo ran-swedish" &&
164 + git förgrena >output &&
165 + test_grep "ran-swedish" output
166 +'
167 +
168 +test_expect_success 'UTF-8 alias with CJK characters' '
169 + test_config alias."分支".command "!echo ran-cjk" &&
170 + git 分支 >output &&
171 + test_grep "ran-cjk" output
172 +'
173 +
174 +test_expect_success 'alias with spaces in name' '
175 + test_config alias."test name".command "!echo ran-spaces" &&
176 + git "test name" >output &&
177 + test_grep "ran-spaces" output
178 +'
179 +
180 +test_expect_success 'subsection aliases listed in help -a' '
181 + test_config alias."förgrena".command "!echo test" &&
182 + git help -a >output &&
183 + test_grep "förgrena" output
184 +'
185 +
186 test_done