Skip to content

Commit 9940362

Browse files
committed
Merge branch 'ua/push-remote-group' (early part) into jch
* 'ua/push-remote-group' (early part): push: support pushing to a remote group remote: move remote group resolution to remote.c
2 parents 2461beb + aac9bf0 commit 9940362

File tree

7 files changed

+355
-83
lines changed

7 files changed

+355
-83
lines changed

Documentation/git-push.adoc

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,28 @@ git push [--all | --branches | --mirror | --tags] [--follow-tags] [--atomic] [-n
1818

1919
DESCRIPTION
2020
-----------
21-
22-
Updates one or more branches, tags, or other references in a remote
23-
repository from your local repository, and sends all necessary data
24-
that isn't already on the remote.
21+
Updates one or more branches, tags, or other references in one or more
22+
remote repositories from your local repository, and sends all necessary
23+
data that isn't already on the remote.
2524

2625
The simplest way to push is `git push <remote> <branch>`.
2726
`git push origin main` will push the local `main` branch to the `main`
2827
branch on the remote named `origin`.
2928

30-
The `<repository>` argument defaults to the upstream for the current branch,
31-
or `origin` if there's no configured upstream.
29+
You can also push to multiple remotes at once by using a remote group.
30+
A remote group is a named list of remotes configured via `remotes.<name>`
31+
in your git config:
32+
33+
$ git config remotes.all-remotes "origin gitlab backup"
34+
35+
Then `git push all-remotes` will push to `origin`, `gitlab`, and
36+
`backup` in turn, as if you had run `git push` against each one
37+
individually. Each remote is pushed independently using its own
38+
push mapping configuration. There is a `remotes.<group>` entry in
39+
the configuration file. (See linkgit:git-config[1]).
40+
41+
The `<repository>` argument defaults to the upstream for the current
42+
branch, or `origin` if there's no configured upstream.
3243

3344
To decide which branches, tags, or other refs to push, Git uses
3445
(in order of precedence):
@@ -55,8 +66,10 @@ OPTIONS
5566
_<repository>_::
5667
The "remote" repository that is the destination of a push
5768
operation. This parameter can be either a URL
58-
(see the section <<URLS,GIT URLS>> below) or the name
59-
of a remote (see the section <<REMOTES,REMOTES>> below).
69+
(see the section <<URLS,GIT URLS>> below), the name
70+
of a remote (see the section <<REMOTES,REMOTES>> below),
71+
or the name of a remote group
72+
(see the section <<REMOTE-GROUPS,REMOTE GROUPS>> below).
6073
6174
`<refspec>...`::
6275
Specify what destination ref to update with what source object.
@@ -430,6 +443,50 @@ further recursion will occur. In this case, `only` is treated as `on-demand`.
430443
431444
include::urls-remotes.adoc[]
432445
446+
[[REMOTE-GROUPS]]
447+
REMOTE GROUPS
448+
-------------
449+
450+
A remote group is a named list of remotes configured via `remotes.<name>`
451+
in your git config:
452+
453+
$ git config remotes.all-remotes "r1 r2 r3"
454+
455+
When a group name is given as the `<repository>` argument, the push is
456+
performed to each member remote in turn. The defining principle is:
457+
458+
git push <options> all-remotes <args>
459+
460+
is exactly equivalent to:
461+
462+
git push <options> r1 <args>
463+
git push <options> r2 <args>
464+
...
465+
git push <options> rN <args>
466+
467+
where r1, r2, ..., rN are the members of `all-remotes`. No special
468+
behaviour is added or removed — the group is purely a shorthand for
469+
running the same push command against each member remote individually.
470+
471+
The behaviour upon failure depends on the kind of error encountered:
472+
473+
If a member remote rejects the push, for example due to a
474+
non-fast-forward update, force needed but not given, an existing tag,
475+
or a server-side hook refusing a ref, Git reports the error and continues
476+
pushing to the remaining remotes in the group. The overall exit code is
477+
non-zero if any member push fails.
478+
479+
If a member remote cannot be contacted at all, for example because the
480+
repository does not exist, authentication fails, or the network is
481+
unreachable, the push stops at that point and the remaining remotes
482+
are not attempted.
483+
484+
This means the user is responsible for ensuring that the sequence of
485+
individual pushes makes sense. If `git push r1`` would fail for a given
486+
set of options and arguments, then `git push all-remotes` will fail in
487+
the same way when it reaches r1. The group push does not do anything
488+
special to make a failing individual push succeed.
489+
433490
OUTPUT
434491
------
435492

builtin/fetch.c

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,48 +2138,6 @@ static int get_one_remote_for_fetch(struct remote *remote, void *priv)
21382138
return 0;
21392139
}
21402140

2141-
struct remote_group_data {
2142-
const char *name;
2143-
struct string_list *list;
2144-
};
2145-
2146-
static int get_remote_group(const char *key, const char *value,
2147-
const struct config_context *ctx UNUSED,
2148-
void *priv)
2149-
{
2150-
struct remote_group_data *g = priv;
2151-
2152-
if (skip_prefix(key, "remotes.", &key) && !strcmp(key, g->name)) {
2153-
/* split list by white space */
2154-
while (*value) {
2155-
size_t wordlen = strcspn(value, " \t\n");
2156-
2157-
if (wordlen >= 1)
2158-
string_list_append_nodup(g->list,
2159-
xstrndup(value, wordlen));
2160-
value += wordlen + (value[wordlen] != '\0');
2161-
}
2162-
}
2163-
2164-
return 0;
2165-
}
2166-
2167-
static int add_remote_or_group(const char *name, struct string_list *list)
2168-
{
2169-
int prev_nr = list->nr;
2170-
struct remote_group_data g;
2171-
g.name = name; g.list = list;
2172-
2173-
repo_config(the_repository, get_remote_group, &g);
2174-
if (list->nr == prev_nr) {
2175-
struct remote *remote = remote_get(name);
2176-
if (!remote_is_configured(remote, 0))
2177-
return 0;
2178-
string_list_append(list, remote->name);
2179-
}
2180-
return 1;
2181-
}
2182-
21832141
static void add_options_to_argv(struct strvec *argv,
21842142
const struct fetch_config *config)
21852143
{

builtin/push.c

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -552,12 +552,13 @@ int cmd_push(int argc,
552552
int flags = 0;
553553
int tags = 0;
554554
int push_cert = -1;
555-
int rc;
555+
int rc = 0;
556+
int base_flags;
556557
const char *repo = NULL; /* default repository */
557558
struct string_list push_options_cmdline = STRING_LIST_INIT_DUP;
559+
struct string_list remote_group = STRING_LIST_INIT_DUP;
558560
struct string_list *push_options;
559561
const struct string_list_item *item;
560-
struct remote *remote;
561562

562563
struct option options[] = {
563564
OPT__VERBOSITY(&verbosity),
@@ -620,39 +621,45 @@ int cmd_push(int argc,
620621
else if (recurse_submodules == RECURSE_SUBMODULES_ONLY)
621622
flags |= TRANSPORT_RECURSE_SUBMODULES_ONLY;
622623

623-
if (tags)
624-
refspec_append(&rs, "refs/tags/*");
625-
626624
if (argc > 0)
627625
repo = argv[0];
628626

629-
remote = pushremote_get(repo);
630-
if (!remote) {
631-
if (repo)
632-
die(_("bad repository '%s'"), repo);
633-
die(_("No configured push destination.\n"
634-
"Either specify the URL from the command-line or configure a remote repository using\n"
635-
"\n"
636-
" git remote add <name> <url>\n"
637-
"\n"
638-
"and then push using the remote name\n"
639-
"\n"
640-
" git push <name>\n"));
641-
}
642-
643-
if (argc > 0)
644-
set_refspecs(argv + 1, argc - 1, remote);
645-
646-
if (remote->mirror)
647-
flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE);
648-
649-
if (flags & TRANSPORT_PUSH_ALL) {
650-
if (argc >= 2)
651-
die(_("--all can't be combined with refspecs"));
652-
}
653-
if (flags & TRANSPORT_PUSH_MIRROR) {
654-
if (argc >= 2)
655-
die(_("--mirror can't be combined with refspecs"));
627+
if (repo) {
628+
if (!add_remote_or_group(repo, &remote_group)) {
629+
/*
630+
* Not a configured remote name or group name.
631+
* Try treating it as a direct URL or path, e.g.
632+
* git push /tmp/foo.git
633+
* git push https://github.com/user/repo.git
634+
* pushremote_get() creates an anonymous remote
635+
* from the URL so the loop below can handle it
636+
* identically to a named remote.
637+
*/
638+
struct remote *r = pushremote_get(repo);
639+
if (!r)
640+
die(_("bad repository '%s'"), repo);
641+
string_list_append(&remote_group, r->name);
642+
}
643+
} else {
644+
struct remote *r = pushremote_get(NULL);
645+
if (!r)
646+
die(_("No configured push destination.\n"
647+
"Either specify the URL from the command-line or configure a remote repository using\n"
648+
"\n"
649+
" git remote add <name> <url>\n"
650+
"\n"
651+
"and then push using the remote name\n"
652+
"\n"
653+
" git push <name>\n"
654+
"\n"
655+
"To push to multiple remotes at once, configure a remote group using\n"
656+
"\n"
657+
" git config remotes.<groupname> \"<remote1> <remote2>\"\n"
658+
"\n"
659+
"and then push using the group name\n"
660+
"\n"
661+
" git push <groupname>\n"));
662+
string_list_append(&remote_group, r->name);
656663
}
657664

658665
if (!is_empty_cas(&cas) && (flags & TRANSPORT_PUSH_FORCE_IF_INCLUDES))
@@ -662,10 +669,60 @@ int cmd_push(int argc,
662669
if (strchr(item->string, '\n'))
663670
die(_("push options must not have new line characters"));
664671

665-
rc = do_push(flags, push_options, remote);
672+
/*
673+
* Push to each remote in remote_group. For a plain "git push <remote>"
674+
* or a default push, remote_group has exactly one entry and the loop
675+
* runs once — there is nothing structurally special about that case.
676+
* For a group, the loop runs once per member remote.
677+
*
678+
* Mirror detection and the --mirror/--all + refspec conflict checks
679+
* are done per remote inside the loop. A remote configured with
680+
* remote.NAME.mirror=true implies mirror mode for that remote only —
681+
* other non-mirror remotes in the same group are unaffected.
682+
*
683+
* rs is rebuilt from scratch for each remote so that per-remote push
684+
* mappings (remote.NAME.push config) are resolved against the correct
685+
* remote. iter_flags is derived from a clean snapshot of flags taken
686+
* before the loop so that a mirror remote cannot bleed
687+
* TRANSPORT_PUSH_FORCE into subsequent non-mirror remotes in the
688+
* same group.
689+
*/
690+
base_flags = flags;
691+
for (size_t i = 0; i < remote_group.nr; i++) {
692+
int iter_flags = base_flags;
693+
struct remote *r = pushremote_get(remote_group.items[i].string);
694+
if (!r)
695+
die(_("no such remote or remote group: %s"),
696+
remote_group.items[i].string);
697+
698+
if (r->mirror)
699+
iter_flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE);
700+
701+
if (iter_flags & TRANSPORT_PUSH_ALL) {
702+
if (argc >= 2)
703+
die(_("--all can't be combined with refspecs"));
704+
}
705+
if (iter_flags & TRANSPORT_PUSH_MIRROR) {
706+
if (argc >= 2)
707+
die(_("--mirror can't be combined with refspecs"));
708+
}
709+
710+
refspec_clear(&rs);
711+
rs = (struct refspec) REFSPEC_INIT_PUSH;
712+
713+
if (tags)
714+
refspec_append(&rs, "refs/tags/*");
715+
if (argc > 0)
716+
set_refspecs(argv + 1, argc - 1, r);
717+
718+
rc |= do_push(iter_flags, push_options, r);
719+
}
720+
666721
string_list_clear(&push_options_cmdline, 0);
667722
string_list_clear(&push_options_config, 0);
723+
string_list_clear(&remote_group, 0);
668724
clear_cas_option(&cas);
725+
669726
if (rc == -1)
670727
usage_with_options(push_usage, options);
671728
else

remote.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2114,6 +2114,43 @@ int get_fetch_map(const struct ref *remote_refs,
21142114
return 0;
21152115
}
21162116

2117+
int get_remote_group(const char *key, const char *value,
2118+
const struct config_context *ctx UNUSED,
2119+
void *priv)
2120+
{
2121+
struct remote_group_data *g = priv;
2122+
2123+
if (skip_prefix(key, "remotes.", &key) && !strcmp(key, g->name)) {
2124+
/* split list by white space */
2125+
while (*value) {
2126+
size_t wordlen = strcspn(value, " \t\n");
2127+
2128+
if (wordlen >= 1)
2129+
string_list_append_nodup(g->list,
2130+
xstrndup(value, wordlen));
2131+
value += wordlen + (value[wordlen] != '\0');
2132+
}
2133+
}
2134+
2135+
return 0;
2136+
}
2137+
2138+
int add_remote_or_group(const char *name, struct string_list *list)
2139+
{
2140+
int prev_nr = list->nr;
2141+
struct remote_group_data g;
2142+
g.name = name; g.list = list;
2143+
2144+
repo_config(the_repository, get_remote_group, &g);
2145+
if (list->nr == prev_nr) {
2146+
struct remote *remote = remote_get(name);
2147+
if (!remote_is_configured(remote, 0))
2148+
return 0;
2149+
string_list_append(list, remote->name);
2150+
}
2151+
return 1;
2152+
}
2153+
21172154
int resolve_remote_symref(struct ref *ref, struct ref *list)
21182155
{
21192156
if (!ref->symref)

remote.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,18 @@ int branch_has_merge_config(struct branch *branch);
347347

348348
int branch_merge_matches(struct branch *, int n, const char *);
349349

350+
/* list of the remote in a group as configured */
351+
struct remote_group_data {
352+
const char *name;
353+
struct string_list *list;
354+
};
355+
356+
int get_remote_group(const char *key, const char *value,
357+
const struct config_context *ctx,
358+
void *priv);
359+
360+
int add_remote_or_group(const char *name, struct string_list *list);
361+
350362
/**
351363
* Return the fully-qualified refname of the tracking branch for `branch`.
352364
* I.e., what "branch@{upstream}" would give you. Returns NULL if no

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,7 @@ integration_tests = [
704704
't5563-simple-http-auth.sh',
705705
't5564-http-proxy.sh',
706706
't5565-push-multiple.sh',
707+
't5566-push-group.sh',
707708
't5570-git-daemon.sh',
708709
't5571-pre-push-hook.sh',
709710
't5572-pull-submodule.sh',

0 commit comments

Comments
 (0)