Merge branch 'jh/alias-i18n' into jch

Extend the alias configuration syntax to allow aliases using
characters outside ASCII alphanumeric (plus '-').

* jh/alias-i18n:
  completion: fix zsh alias listing for subsection aliases
  alias: support non-alphanumeric names via subsection syntax
  alias: prepare for subsection aliases
  help: use list_aliases() for alias listing
This commit is contained in:
Junio C Hamano
2026-02-23 14:25:45 -08:00
6 changed files with 182 additions and 35 deletions

View File

@@ -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

42
alias.c
View File

@@ -13,23 +13,53 @@ 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) {
string_list_append(data->list, p);
struct string_list_item *item;
if (!value)
return config_error_nonbool(var);
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);
}
return 0;

View File

@@ -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);

View File

@@ -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
}

39
help.c
View File

@@ -20,6 +20,8 @@
#include "prompt.h"
#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 */
@@ -107,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);
@@ -468,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;
@@ -501,11 +489,11 @@ 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++) {
size_t len = strlen(alias_list.items[i].string);
size_t len = utf8_strwidth(alias_list.items[i].string);
if (longest < len)
longest = len;
}
@@ -586,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);
@@ -601,8 +590,18 @@ 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) {
/* [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;
}

View File

@@ -112,4 +112,75 @@ 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_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