Skip to content

Commit ac1f12a

Browse files
jonatan-holmgrengitster
authored andcommitted
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>
1 parent 2ad33ea commit ac1f12a

4 files changed

Lines changed: 145 additions & 18 deletions

File tree

Documentation/config/alias.adoc

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,46 @@
11
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.
1044
+
1145
Note that the first word of an alias does not necessarily have to be a
1246
command. It can be a command-line option that will be passed into the

alias.c

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,52 @@ struct config_alias_data {
1313
struct string_list *list;
1414
};
1515

16-
static int config_alias_cb(const char *key, const char *value,
16+
static int config_alias_cb(const char *var, const char *value,
1717
const struct config_context *ctx UNUSED, void *d)
1818
{
1919
struct config_alias_data *data = d;
20-
const char *p;
20+
const char *subsection, *key;
21+
size_t subsection_len;
2122

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"))
2334
return 0;
2435

2536
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) {
2747
FREE_AND_NULL(data->v);
2848
return git_config_string(&data->v,
29-
key, value);
49+
var, value);
3050
}
3151
} else if (data->list) {
3252
struct string_list_item *item;
3353

3454
if (!value)
35-
return config_error_nonbool(key);
55+
return config_error_nonbool(var);
3656

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);
3862
item->util = xstrdup(value);
3963
}
4064

help.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "fsmonitor-ipc.h"
2222
#include "repository.h"
2323
#include "alias.h"
24+
#include "utf8.h"
2425

2526
#ifndef NO_CURL
2627
#include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
108109

109110
for (i = 0; cmds[i].name; i++) {
110111
if (cmds[i].category & mask) {
111-
size_t len = strlen(cmds[i].name);
112+
size_t len = utf8_strwidth(cmds[i].name);
112113
printf(" %s ", cmds[i].name);
113114
if (longest > len)
114115
mput_char(' ', longest - len);
@@ -493,7 +494,7 @@ static void list_all_cmds_help_aliases(int longest)
493494
string_list_sort(&alias_list);
494495

495496
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);
497498
if (longest < len)
498499
longest = len;
499500
}
@@ -592,8 +593,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
592593
/* Also use aliases for command lookup */
593594
if (!parse_config_key(var, "alias", &subsection, &subsection_len,
594595
&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 */
596603
add_cmdname(&cfg->aliases, key, strlen(key));
604+
}
597605
}
598606

599607
return 0;

t/t0014-alias.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '
122122
test_grep "alias.noval" error
123123
'
124124

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+
125186
test_done

0 commit comments

Comments
 (0)