From 3f0cdfa87907096ed7c6caa33fbf360e0e19844c Mon Sep 17 00:00:00 2001 From: Jonatan Holmgren Date: Wed, 18 Feb 2026 22:57:34 +0100 Subject: [PATCH 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 Signed-off-by: Junio C Hamano --- 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 3c36d9c218..84b9e5efe4 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 */ @@ -469,20 +470,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; @@ -502,7 +489,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 From 2ad33ea6b5c0a5670a757c9b594cc07321324e80 Mon Sep 17 00:00:00 2001 From: Jonatan Holmgren Date: Wed, 18 Feb 2026 22:57:35 +0100 Subject: [PATCH 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.." as defining an alias ".". With this change, alias.. entries are silently ignored (unless 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 Signed-off-by: Junio C Hamano --- help.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/help.c b/help.c index 84b9e5efe4..a781ebb98d 100644 --- a/help.c +++ b/help.c @@ -574,7 +574,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); @@ -589,8 +590,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; } From ac1f12a9de4b79b176a08f524fecdb092ff00e74 Mon Sep 17 00:00:00 2001 From: Jonatan Holmgren Date: Wed, 18 Feb 2026 22:57:36 +0100 Subject: [PATCH 3/4] alias: support non-alphanumeric names via subsection syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Jonatan Holmgren Signed-off-by: Junio C Hamano --- 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..115fdbb1e3 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 a781ebb98d..82fb2eaa3f 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); @@ -493,7 +494,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; } @@ -592,8 +593,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 From edd8ad18a643d47dd92b08ab865bf7f4a26f50bc Mon Sep 17 00:00:00 2001 From: Jonatan Holmgren Date: Wed, 18 Feb 2026 22:57:37 +0100 Subject: [PATCH 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 Signed-off-by: Junio C Hamano --- 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 }