From: Usman Akinyemi Date: Thu, 05 Mar 2026 22:32:48 GMT Subject: [RFC PATCH 2/2] push: support pushing to a remote group Message-ID: <20260305223248.170785-3-usmanakinyemi202@gmail.com> In-Reply-To: <20260305223248.170785-1-usmanakinyemi202@gmail.com> `git fetch` accepts a remote group name (configured via `remotes.` in config) and fetches from each member remote. `git push` has no equivalent — it only accepts a single remote name. Teach `git push` to resolve its repository argument through `add_remote_or_group()`, which was made public in the previous patch, so that a user can push to all remotes in a group with: git push When the argument resolves to a single remote the behaviour is identical to before. When it resolves to a group, each member remote is pushed in sequence. The group push path rebuilds the refspec list (`rs`) from scratch for each member remote so that per-remote push mappings configured via `remote..push` are resolved correctly against each specific remote. Without this, refspec entries would accumulate across iterations and each subsequent remote would receive a growing list of duplicated entries. Mirror detection (`remote->mirror`) is also evaluated per remote using a copy of the flags, so that a mirror remote in the group cannot set TRANSPORT_PUSH_FORCE on subsequent non-mirror remotes in the same group. A known interaction: push.default = simple will die when the current branch has no upstream configured, because setup_default_push_refspecs() requires an upstream for that mode. Users pushing to a group should set push.default = current or supply explicit refspecs. This is consistent with how fetch handles default refspec resolution per remote. Suggested-by: Junio C Hamano Signed-off-by: Usman Akinyemi --- builtin/push.c | 89 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/builtin/push.c b/builtin/push.c index 5b6cebbb85..a98fb4c934 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -551,12 +551,13 @@ int cmd_push(int argc, int flags = 0; int tags = 0; int push_cert = -1; - int rc; + int rc = 0; const char *repo = NULL; /* default repository */ struct string_list push_options_cmdline = STRING_LIST_INIT_DUP; + struct string_list remote_group = STRING_LIST_INIT_DUP; /* represent remote or remote group */ struct string_list *push_options; const struct string_list_item *item; - struct remote *remote; + struct remote *remote = NULL; struct option options[] = { OPT__VERBOSITY(&verbosity), @@ -625,25 +626,35 @@ int cmd_push(int argc, if (argc > 0) repo = argv[0]; - remote = pushremote_get(repo); - if (!remote) { - if (repo) - die(_("bad repository '%s'"), repo); - die(_("No configured push destination.\n" - "Either specify the URL from the command-line or configure a remote repository using\n" - "\n" - " git remote add \n" - "\n" - "and then push using the remote name\n" - "\n" - " git push \n")); + if (repo) { + if (!add_remote_or_group(repo, &remote_group)) + die(_("no such remote or remote group: %s"), repo); + } else { + remote = pushremote_get(NULL); + if (!remote) + die(_("No configured push destination.\n" + "Either specify the URL from the command-line or configure a remote repository using\n" + "\n" + " git remote add \n" + "\n" + "and then push using the remote name\n" + "\n" + " git push \n")); } - if (argc > 0) - set_refspecs(argv + 1, argc - 1, remote); + /* + * set_refspecs and mirror detection must not use `remote` + * when it may be NULL (group path). For the single-remote case, + * handle them here. For the group case they are handled + * per-remote inside the loop below. + */ + if (remote) { + if (argc > 0) + set_refspecs(argv + 1, argc - 1, remote); - if (remote->mirror) - flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE); + if (remote->mirror) + flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE); + } if (flags & TRANSPORT_PUSH_ALL) { if (argc >= 2) @@ -661,10 +672,50 @@ int cmd_push(int argc, if (strchr(item->string, '\n')) die(_("push options must not have new line characters")); - rc = do_push(flags, push_options, remote); + if (remote) { + rc = do_push(flags, push_options, remote); + } else { + int base_flags = flags; + for (int i = 0; i < remote_group.nr; i++) { + int iter_flags = base_flags; + struct remote *r = pushremote_get(remote_group.items[i].string); + if (!r) + die(_("no such remote or remote group: %s"), + remote_group.items[i].string); + + /* + * Rebuild rs from scratch for each remote so that + * push mappings (remote.NAME.push config) are resolved + * against this specific remote. Without this, mappings + * from a previous iteration would accumulate in rs and + * each remote would be pushed an ever-growing refspec list. + */ + refspec_clear(&rs); + rs = (struct refspec) REFSPEC_INIT_PUSH; + + if (tags) + refspec_append(&rs, "refs/tags/*"); + if (argc > 0) + set_refspecs(argv + 1, argc - 1, r); + + /* + * Compute mirror flag from a fresh base each iteration + * so that a mirror remote does not bleed TRANSPORT_PUSH_FORCE + * into subsequent non-mirror remotes in the same group. + */ + if (r->mirror) + iter_flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE); + + rc |= do_push(iter_flags, push_options, r); + } + } + + string_list_clear(&push_options_cmdline, 0); string_list_clear(&push_options_config, 0); + string_list_clear(&remote_group, 0); clear_cas_option(&cas); + if (rc == -1) usage_with_options(push_usage, options); else -- 2.53.0