On Thu, Sep 11, 2025 at 07:31:04PM +0200, Kristoffer Haugsbakk wrote: > But it turns out there is a regression in my code with the recursion > detection. Compare: > > $ ./git -c alias.one=two -c alias.two=two one -h > 'one' is aliased to 'two' > 'two' is aliased to 'two' > fatal: recursive alias: two Your example there is a little funny; "two" is recursive to itself, even without "one". We detect that case in handle_alias(), and we would still detect: git -c alias.whatchanged=whatchanged whatchanged -h in the same spot. I assume what you meant was: $ ./git -c alias.one=two -c alias.two=one one -h 'one' is aliased to 'two' 'two' is aliased to 'one' fatal: alias loop detected: expansion of 'one' does not terminate: one <== two ==> which is handled in the caller. And indeed... > $ ./git -c alias.whatchanged=pack-redundant -c alias.pack-redundant=whatchanged whatchanged -h > 'whatchanged' is aliased to 'pack-redundant' > 'pack-redundant' is aliased to 'whatchanged' > 'whatchanged' is aliased to 'pack-redundant' > 'pack-redundant' is aliased to 'whatchanged' > 'whatchanged' is aliased to 'pack-redundant' > 'pack-redundant' is aliased to 'whatchanged' > 'whatchanged' is aliased to 'pack-redundant' > 'pack-redundant' is aliased to 'whatchanged' > [forever] ...that cannot work with where we've placed the new conditional, because it continues the loop before hitting the alias check. If we move that to the top, like the patch below, it works. It is a little funny to do it up front even when we're going to directly run a builtin, but I don't think it harms much. OTOH I find the whole placement a little odd in the first place. We are speculatively adding to the cmd list before even finding out if we have an alias. It kind of feels like this should be part of handle_alias() in the first place. We'd need to pass in the cmd_list variable for it to add to and check. -Peff --- diff --git a/git.c b/git.c index ca66e24639..17c566a802 100644 --- a/git.c +++ b/git.c @@ -814,6 +814,24 @@ static int run_argv(struct strvec *args) struct string_list_item *seen; while (1) { + seen = unsorted_string_list_lookup(&cmd_list, args->v[0]); + if (seen) { + struct strbuf sb = STRBUF_INIT; + for (size_t i = 0; i < cmd_list.nr; i++) { + struct string_list_item *item = &cmd_list.items[i]; + + strbuf_addf(&sb, "\n %s", item->string); + if (item == seen) + strbuf_addstr(&sb, " <=="); + else if (i == cmd_list.nr - 1) + strbuf_addstr(&sb, " ==>"); + } + die(_("alias loop detected: expansion of '%s' does" + " not terminate:%s"), cmd_list.items[0].string, sb.buf); + } + + string_list_append(&cmd_list, args->v[0]); + /* * Allow deprecated commands to be overridden by aliases. This * creates a seamless path forward for people who want to keep @@ -874,24 +892,6 @@ static int run_argv(struct strvec *args) /* .. then try the external ones */ execv_dashed_external(args->v); - seen = unsorted_string_list_lookup(&cmd_list, args->v[0]); - if (seen) { - struct strbuf sb = STRBUF_INIT; - for (size_t i = 0; i < cmd_list.nr; i++) { - struct string_list_item *item = &cmd_list.items[i]; - - strbuf_addf(&sb, "\n %s", item->string); - if (item == seen) - strbuf_addstr(&sb, " <=="); - else if (i == cmd_list.nr - 1) - strbuf_addstr(&sb, " ==>"); - } - die(_("alias loop detected: expansion of '%s' does" - " not terminate:%s"), cmd_list.items[0].string, sb.buf); - } - - string_list_append(&cmd_list, args->v[0]); - /* * It could be an alias -- this works around the insanity * of overriding "git log" with "git show" by having