Skip to content

Commit d7e6f93

Browse files
Unique-Usmangitster
authored andcommitted
push: support pushing to a remote group
`git fetch` accepts a remote group name (configured via `remotes.<name>` 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 <group> 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.<name>.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 <gitster@pobox.com> Signed-off-by: Usman Akinyemi <usmanakinyemi202@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent fe92cd8 commit d7e6f93

4 files changed

Lines changed: 254 additions & 42 deletions

File tree

Documentation/git-push.adoc

Lines changed: 68 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,53 @@ 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+
This means the user is responsible for ensuring that the sequence of
472+
individual pushes makes sense. For example, if `push.default = simple`
473+
is set and the current branch has no upstream configured, then
474+
`git push r1` may fail. `git push all-remotes` will fail in the same
475+
way, on whichever member remote triggers the condition first. Setting
476+
`push.default = current` or supplying explicit refspecs is recommended
477+
when pushing to a remote group.
478+
479+
Similarly, if `--force-with-lease` is given without an explicit expected
480+
commit, Git will guess the expected commit for each remote independently
481+
from that remote's own remote-tracking branch, the same way it would if
482+
each push were run separately. If an explicit commit is given with
483+
`--force-with-lease=<refname>:<expect>`, that same value is forwarded
484+
to every member remote, as if each of
485+
`git push --force-with-lease=<refname>:<expect> r1`,
486+
`git push --force-with-lease=<refname>:<expect> r2`, ...,
487+
`git push --force-with-lease=<refname>:<expect> rN` had been invoked.
488+
489+
Each member remote is pushed using its own push mapping configuration
490+
(`remote.<name>.push`), so a refspec that maps differently on r1 than
491+
on r2 is resolved correctly for each one.
492+
433493
OUTPUT
434494
------
435495

builtin/push.c

Lines changed: 90 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,6 @@ static int git_push_config(const char *k, const char *v,
543543

544544
return git_default_config(k, v, ctx, NULL);
545545
}
546-
547546
int cmd_push(int argc,
548547
const char **argv,
549548
const char *prefix,
@@ -552,12 +551,13 @@ int cmd_push(int argc,
552551
int flags = 0;
553552
int tags = 0;
554553
int push_cert = -1;
555-
int rc;
554+
int rc = 0;
555+
int base_flags;
556556
const char *repo = NULL; /* default repository */
557557
struct string_list push_options_cmdline = STRING_LIST_INIT_DUP;
558+
struct string_list remote_group = STRING_LIST_INIT_DUP;
558559
struct string_list *push_options;
559560
const struct string_list_item *item;
560-
struct remote *remote;
561561

562562
struct option options[] = {
563563
OPT__VERBOSITY(&verbosity),
@@ -620,39 +620,45 @@ int cmd_push(int argc,
620620
else if (recurse_submodules == RECURSE_SUBMODULES_ONLY)
621621
flags |= TRANSPORT_RECURSE_SUBMODULES_ONLY;
622622

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

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

658664
if (!is_empty_cas(&cas) && (flags & TRANSPORT_PUSH_FORCE_IF_INCLUDES))
@@ -662,10 +668,60 @@ int cmd_push(int argc,
662668
if (strchr(item->string, '\n'))
663669
die(_("push options must not have new line characters"));
664670

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

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ integration_tests = [
700700
't5563-simple-http-auth.sh',
701701
't5564-http-proxy.sh',
702702
't5565-push-multiple.sh',
703+
't5566-push-group.sh',
703704
't5570-git-daemon.sh',
704705
't5571-pre-push-hook.sh',
705706
't5572-pull-submodule.sh',

t/t5566-push-group.sh

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/bin/sh
2+
3+
test_description='push to remote group'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'setup' '
8+
for i in 1 2 3
9+
do
10+
git init --bare dest-$i.git &&
11+
git -C dest-$i.git symbolic-ref HEAD refs/heads/not-a-branch ||
12+
return 1
13+
done &&
14+
test_tick &&
15+
git commit --allow-empty -m "initial" &&
16+
git config set remote.remote-1.url "file://$(pwd)/dest-1.git" &&
17+
git config set remote.remote-1.fetch "+refs/heads/*:refs/remotes/remote-1/*" &&
18+
git config set remote.remote-2.url "file://$(pwd)/dest-2.git" &&
19+
git config set remote.remote-2.fetch "+refs/heads/*:refs/remotes/remote-2/*" &&
20+
git config set remote.remote-3.url "file://$(pwd)/dest-3.git" &&
21+
git config set remote.remote-3.fetch "+refs/heads/*:refs/remotes/remote-3/*" &&
22+
git config set remotes.all-remotes "remote-1 remote-2 remote-3"
23+
'
24+
25+
test_expect_success 'push to remote group pushes to all members' '
26+
git push all-remotes HEAD:refs/heads/main &&
27+
j= &&
28+
for i in 1 2 3
29+
do
30+
git -C dest-$i.git for-each-ref >actual-$i &&
31+
if test -n "$j"
32+
then
33+
test_cmp actual-$j actual-$i
34+
else
35+
cat actual-$i
36+
fi &&
37+
j=$i ||
38+
return 1
39+
done
40+
'
41+
42+
test_expect_success 'push second commit to group updates all members' '
43+
test_tick &&
44+
git commit --allow-empty -m "second" &&
45+
git push all-remotes HEAD:refs/heads/main &&
46+
for i in 1 2 3
47+
do
48+
git -C dest-$i.git rev-parse refs/heads/main >hash-$i ||
49+
return 1
50+
done &&
51+
test_cmp hash-1 hash-2 &&
52+
test_cmp hash-2 hash-3
53+
'
54+
55+
test_expect_success 'push to single remote in group does not affect others' '
56+
test_tick &&
57+
git commit --allow-empty -m "third" &&
58+
git push remote-1 HEAD:refs/heads/main &&
59+
git -C dest-1.git rev-parse refs/heads/main >hash-after-1 &&
60+
git -C dest-2.git rev-parse refs/heads/main >hash-after-2 &&
61+
! test_cmp hash-after-1 hash-after-2
62+
'
63+
64+
test_expect_success 'push to nonexistent group fails with error' '
65+
test_must_fail git push no-such-group HEAD:refs/heads/main
66+
'
67+
68+
test_expect_success 'push explicit refspec to group' '
69+
test_tick &&
70+
git commit --allow-empty -m "fourth" &&
71+
git push all-remotes HEAD:refs/heads/other &&
72+
for i in 1 2 3
73+
do
74+
git -C dest-$i.git rev-parse refs/heads/other >other-hash-$i ||
75+
return 1
76+
done &&
77+
test_cmp other-hash-1 other-hash-2 &&
78+
test_cmp other-hash-2 other-hash-3
79+
'
80+
81+
test_expect_success 'mirror remote in group with refspec fails' '
82+
git config set remote.remote-1.mirror true &&
83+
test_must_fail git push all-remotes HEAD:refs/heads/main 2>err &&
84+
grep "mirror" err &&
85+
git config unset remote.remote-1.mirror
86+
'
87+
test_expect_success 'push.default=current works with group push' '
88+
git config set push.default current &&
89+
test_tick &&
90+
git commit --allow-empty -m "fifth" &&
91+
git push all-remotes &&
92+
git config unset push.default
93+
'
94+
95+
test_done

0 commit comments

Comments
 (0)