Merge branch 'ua/push-remote-group'
"git push" learned to take a "remote group" name to push to, which causes pushes to multiple places, just like "git fetch" would do. * ua/push-remote-group: push: support pushing to a remote group remote: move remote group resolution to remote.c remote: fix sign-compare warnings in push_cas_optionmain
commit
2c677d20b6
|
|
@ -18,17 +18,28 @@ git push [--all | --branches | --mirror | --tags] [--follow-tags] [--atomic] [-n
|
|||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
|
||||
Updates one or more branches, tags, or other references in a remote
|
||||
repository from your local repository, and sends all necessary data
|
||||
that isn't already on the remote.
|
||||
Updates one or more branches, tags, or other references in one or more
|
||||
remote repositories from your local repository, and sends all necessary
|
||||
data that isn't already on the remote.
|
||||
|
||||
The simplest way to push is `git push <remote> <branch>`.
|
||||
`git push origin main` will push the local `main` branch to the `main`
|
||||
branch on the remote named `origin`.
|
||||
|
||||
The `<repository>` argument defaults to the upstream for the current branch,
|
||||
or `origin` if there's no configured upstream.
|
||||
You can also push to multiple remotes at once by using a remote group.
|
||||
A remote group is a named list of remotes configured via `remotes.<name>`
|
||||
in your git config:
|
||||
|
||||
$ git config remotes.all-remotes "origin gitlab backup"
|
||||
|
||||
Then `git push all-remotes` will push to `origin`, `gitlab`, and
|
||||
`backup` in turn, as if you had run `git push` against each one
|
||||
individually. Each remote is pushed independently using its own
|
||||
push mapping configuration. There is a `remotes.<group>` entry in
|
||||
the configuration file. (See linkgit:git-config[1]).
|
||||
|
||||
The `<repository>` argument defaults to the upstream for the current
|
||||
branch, or `origin` if there's no configured upstream.
|
||||
|
||||
To decide which branches, tags, or other refs to push, Git uses
|
||||
(in order of precedence):
|
||||
|
|
@ -55,8 +66,10 @@ OPTIONS
|
|||
_<repository>_::
|
||||
The "remote" repository that is the destination of a push
|
||||
operation. This parameter can be either a URL
|
||||
(see the section <<URLS,GIT URLS>> below) or the name
|
||||
of a remote (see the section <<REMOTES,REMOTES>> below).
|
||||
(see the section <<URLS,GIT URLS>> below), the name
|
||||
of a remote (see the section <<REMOTES,REMOTES>> below),
|
||||
or the name of a remote group
|
||||
(see the section <<REMOTE-GROUPS,REMOTE GROUPS>> below).
|
||||
|
||||
`<refspec>...`::
|
||||
Specify what destination ref to update with what source object.
|
||||
|
|
@ -430,6 +443,57 @@ further recursion will occur. In this case, `only` is treated as `on-demand`.
|
|||
|
||||
include::urls-remotes.adoc[]
|
||||
|
||||
[[REMOTE-GROUPS]]
|
||||
REMOTE GROUPS
|
||||
-------------
|
||||
|
||||
A remote group is a named list of remotes configured via `remotes.<name>`
|
||||
in your git config:
|
||||
|
||||
$ git config remotes.all-remotes "r1 r2 r3"
|
||||
|
||||
When a group name is given as the `<repository>` argument, the push is
|
||||
performed to each member remote in turn. The defining principle is:
|
||||
|
||||
git push <options> all-remotes <args>
|
||||
|
||||
is exactly equivalent to:
|
||||
|
||||
git push <options> r1 <args>
|
||||
git push <options> r2 <args>
|
||||
...
|
||||
git push <options> rN <args>
|
||||
|
||||
where r1, r2, ..., rN are the members of `all-remotes`. No special
|
||||
behaviour is added or removed — the group is purely a shorthand for
|
||||
running the same push command against each member remote individually.
|
||||
|
||||
When pushing to a group of more than one remote, Git spawns a separate
|
||||
`git push` subprocess for each member remote in sequence. Each subprocess
|
||||
receives the same flags and refspecs as the original invocation. This
|
||||
means that per-remote push mappings configured via `remote.<name>.push`
|
||||
and mirror mode (`remote.<name>.mirror`) are evaluated independently for
|
||||
each remote, and a mirror remote in the group cannot affect the push
|
||||
behaviour of other non-mirror remotes in the same group.
|
||||
|
||||
The `--atomic` option is not supported for group pushes, because atomicity
|
||||
can only be guaranteed within a single transport connection to a single
|
||||
remote. Git will refuse the invocation with an error if `--atomic` is
|
||||
combined with a group name.
|
||||
|
||||
If any member remote fails whether due to a push rejection (e.g. a
|
||||
non-fast-forward update, a server-side hook refusing a ref) or a connection
|
||||
error (e.g. the repository does not exist, authentication fails, or the
|
||||
network is unreachable), Git reports the error and continues pushing to
|
||||
the remaining remotes in the group. The overall exit code is non-zero if
|
||||
any member push fails.
|
||||
|
||||
This means the user is responsible for ensuring that the sequence of
|
||||
individual pushes makes sense. If `git push r1`` would fail for a given
|
||||
set of options and arguments, then `git push all-remotes` will fail in
|
||||
the same way when it reaches r1. The group push does not do anything
|
||||
special to make a failing individual push succeed.
|
||||
|
||||
OUTPUT
|
||||
------
|
||||
|
||||
|
|
|
|||
|
|
@ -2184,48 +2184,6 @@ static int get_one_remote_for_fetch(struct remote *remote, void *priv)
|
|||
return 0;
|
||||
}
|
||||
|
||||
struct remote_group_data {
|
||||
const char *name;
|
||||
struct string_list *list;
|
||||
};
|
||||
|
||||
static int get_remote_group(const char *key, const char *value,
|
||||
const struct config_context *ctx UNUSED,
|
||||
void *priv)
|
||||
{
|
||||
struct remote_group_data *g = priv;
|
||||
|
||||
if (skip_prefix(key, "remotes.", &key) && !strcmp(key, g->name)) {
|
||||
/* split list by white space */
|
||||
while (*value) {
|
||||
size_t wordlen = strcspn(value, " \t\n");
|
||||
|
||||
if (wordlen >= 1)
|
||||
string_list_append_nodup(g->list,
|
||||
xstrndup(value, wordlen));
|
||||
value += wordlen + (value[wordlen] != '\0');
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_remote_or_group(const char *name, struct string_list *list)
|
||||
{
|
||||
int prev_nr = list->nr;
|
||||
struct remote_group_data g;
|
||||
g.name = name; g.list = list;
|
||||
|
||||
repo_config(the_repository, get_remote_group, &g);
|
||||
if (list->nr == prev_nr) {
|
||||
struct remote *remote = remote_get(name);
|
||||
if (!remote_is_configured(remote, 0))
|
||||
return 0;
|
||||
string_list_append(list, remote->name);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void add_options_to_argv(struct strvec *argv,
|
||||
const struct fetch_config *config)
|
||||
{
|
||||
|
|
|
|||
251
builtin/push.c
251
builtin/push.c
|
|
@ -10,6 +10,7 @@
|
|||
#include "config.h"
|
||||
#include "environment.h"
|
||||
#include "gettext.h"
|
||||
#include "hex.h"
|
||||
#include "refspec.h"
|
||||
#include "run-command.h"
|
||||
#include "remote.h"
|
||||
|
|
@ -544,6 +545,123 @@ static int git_push_config(const char *k, const char *v,
|
|||
return git_default_config(k, v, ctx, NULL);
|
||||
}
|
||||
|
||||
static int push_multiple(struct string_list *list,
|
||||
const struct string_list *push_options,
|
||||
int flags,
|
||||
int tags,
|
||||
const char **refspecs,
|
||||
int refspec_nr)
|
||||
{
|
||||
int result = 0;
|
||||
size_t i;
|
||||
struct strvec argv = STRVEC_INIT;
|
||||
|
||||
strvec_push(&argv, "push");
|
||||
|
||||
if (flags & TRANSPORT_PUSH_FORCE)
|
||||
strvec_push(&argv, "--force");
|
||||
if (flags & TRANSPORT_PUSH_DRY_RUN)
|
||||
strvec_push(&argv, "--dry-run");
|
||||
if (flags & TRANSPORT_PUSH_PORCELAIN)
|
||||
strvec_push(&argv, "--porcelain");
|
||||
if (flags & TRANSPORT_PUSH_PRUNE)
|
||||
strvec_push(&argv, "--prune");
|
||||
if (flags & TRANSPORT_PUSH_NO_HOOK)
|
||||
strvec_push(&argv, "--no-verify");
|
||||
if (flags & TRANSPORT_PUSH_FOLLOW_TAGS)
|
||||
strvec_push(&argv, "--follow-tags");
|
||||
if (flags & TRANSPORT_PUSH_SET_UPSTREAM)
|
||||
strvec_push(&argv, "--set-upstream");
|
||||
if (flags & TRANSPORT_PUSH_FORCE_IF_INCLUDES)
|
||||
strvec_push(&argv, "--force-if-includes");
|
||||
if (flags & TRANSPORT_PUSH_ALL)
|
||||
strvec_push(&argv, "--all");
|
||||
if (flags & TRANSPORT_PUSH_MIRROR)
|
||||
strvec_push(&argv, "--mirror");
|
||||
|
||||
if (flags & TRANSPORT_PUSH_CERT_ALWAYS)
|
||||
strvec_push(&argv, "--signed=yes");
|
||||
else if (flags & TRANSPORT_PUSH_CERT_IF_ASKED)
|
||||
strvec_push(&argv, "--signed=if-asked");
|
||||
if (!thin)
|
||||
strvec_push(&argv, "--no-thin");
|
||||
|
||||
if (deleterefs)
|
||||
strvec_push(&argv, "--delete");
|
||||
|
||||
if (receivepack)
|
||||
strvec_pushf(&argv, "--receive-pack=%s", receivepack);
|
||||
if (verbosity >= 2)
|
||||
strvec_push(&argv, "-v");
|
||||
if (verbosity >= 1)
|
||||
strvec_push(&argv, "-v");
|
||||
else if (verbosity < 0)
|
||||
strvec_push(&argv, "-q");
|
||||
if (progress > 0)
|
||||
strvec_push(&argv, "--progress");
|
||||
else if (progress == 0)
|
||||
strvec_push(&argv, "--no-progress");
|
||||
|
||||
if (family == TRANSPORT_FAMILY_IPV4)
|
||||
strvec_push(&argv, "--ipv4");
|
||||
else if (family == TRANSPORT_FAMILY_IPV6)
|
||||
strvec_push(&argv, "--ipv6");
|
||||
|
||||
if (recurse_submodules == RECURSE_SUBMODULES_CHECK)
|
||||
strvec_push(&argv, "--recurse-submodules=check");
|
||||
else if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)
|
||||
strvec_push(&argv, "--recurse-submodules=on-demand");
|
||||
else if (recurse_submodules == RECURSE_SUBMODULES_ONLY)
|
||||
strvec_push(&argv, "--recurse-submodules=only");
|
||||
else if (recurse_submodules == RECURSE_SUBMODULES_OFF)
|
||||
strvec_push(&argv, "--recurse-submodules=no");
|
||||
|
||||
|
||||
if (tags)
|
||||
strvec_push(&argv, "--tags");
|
||||
|
||||
for (i = 0; i < push_options->nr; i++)
|
||||
strvec_pushf(&argv, "--push-option=%s",
|
||||
push_options->items[i].string);
|
||||
|
||||
for (i = 0; i < cas.nr; i++) {
|
||||
if (cas.entry[i].use_tracking) {
|
||||
strvec_pushf(&argv, "--force-with-lease=%s",
|
||||
cas.entry[i].refname);
|
||||
} else if (!is_null_oid(&cas.entry[i].expect)) {
|
||||
strvec_pushf(&argv, "--force-with-lease=%s:%s",
|
||||
cas.entry[i].refname,
|
||||
oid_to_hex(&cas.entry[i].expect));
|
||||
} else {
|
||||
strvec_push(&argv, "--force-with-lease");
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < list->nr; i++) {
|
||||
const char *name = list->items[i].string;
|
||||
struct child_process cmd = CHILD_PROCESS_INIT;
|
||||
int j;
|
||||
|
||||
strvec_pushv(&cmd.args, argv.v);
|
||||
strvec_push(&cmd.args, name);
|
||||
|
||||
for (j = 0; j < refspec_nr; j++)
|
||||
strvec_push(&cmd.args, refspecs[j]);
|
||||
|
||||
if (verbosity >= 0)
|
||||
printf(_("Pushing to %s\n"), name);
|
||||
|
||||
cmd.git_cmd = 1;
|
||||
if (run_command(&cmd)) {
|
||||
error(_("could not push to %s"), name);
|
||||
result = 1;
|
||||
}
|
||||
}
|
||||
|
||||
strvec_clear(&argv);
|
||||
return result;
|
||||
}
|
||||
|
||||
int cmd_push(int argc,
|
||||
const char **argv,
|
||||
const char *prefix,
|
||||
|
|
@ -552,12 +670,13 @@ int cmd_push(int argc,
|
|||
int flags = 0;
|
||||
int tags = 0;
|
||||
int push_cert = -1;
|
||||
int rc;
|
||||
int rc = 0;
|
||||
int base_flags;
|
||||
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;
|
||||
struct string_list *push_options;
|
||||
const struct string_list_item *item;
|
||||
struct remote *remote;
|
||||
|
||||
struct option options[] = {
|
||||
OPT__VERBOSITY(&verbosity),
|
||||
|
|
@ -620,39 +739,45 @@ int cmd_push(int argc,
|
|||
else if (recurse_submodules == RECURSE_SUBMODULES_ONLY)
|
||||
flags |= TRANSPORT_RECURSE_SUBMODULES_ONLY;
|
||||
|
||||
if (tags)
|
||||
refspec_append(&rs, "refs/tags/*");
|
||||
|
||||
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 <name> <url>\n"
|
||||
"\n"
|
||||
"and then push using the remote name\n"
|
||||
"\n"
|
||||
" git push <name>\n"));
|
||||
}
|
||||
|
||||
if (argc > 0)
|
||||
set_refspecs(argv + 1, argc - 1, remote);
|
||||
|
||||
if (remote->mirror)
|
||||
flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE);
|
||||
|
||||
if (flags & TRANSPORT_PUSH_ALL) {
|
||||
if (argc >= 2)
|
||||
die(_("--all can't be combined with refspecs"));
|
||||
}
|
||||
if (flags & TRANSPORT_PUSH_MIRROR) {
|
||||
if (argc >= 2)
|
||||
die(_("--mirror can't be combined with refspecs"));
|
||||
if (repo) {
|
||||
if (!add_remote_or_group(repo, &remote_group)) {
|
||||
/*
|
||||
* Not a configured remote name or group name.
|
||||
* Try treating it as a direct URL or path, e.g.
|
||||
* git push /tmp/foo.git
|
||||
* git push https://github.com/user/repo.git
|
||||
* pushremote_get() creates an anonymous remote
|
||||
* from the URL so the loop below can handle it
|
||||
* identically to a named remote.
|
||||
*/
|
||||
struct remote *r = pushremote_get(repo);
|
||||
if (!r)
|
||||
die(_("bad repository '%s'"), repo);
|
||||
string_list_append(&remote_group, r->name);
|
||||
}
|
||||
} else {
|
||||
struct remote *r = pushremote_get(NULL);
|
||||
if (!r)
|
||||
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 <name> <url>\n"
|
||||
"\n"
|
||||
"and then push using the remote name\n"
|
||||
"\n"
|
||||
" git push <name>\n"
|
||||
"\n"
|
||||
"To push to multiple remotes at once, configure a remote group using\n"
|
||||
"\n"
|
||||
" git config remotes.<groupname> \"<remote1> <remote2>\"\n"
|
||||
"\n"
|
||||
"and then push using the group name\n"
|
||||
"\n"
|
||||
" git push <groupname>\n"));
|
||||
string_list_append(&remote_group, r->name);
|
||||
}
|
||||
|
||||
if (!is_empty_cas(&cas) && (flags & TRANSPORT_PUSH_FORCE_IF_INCLUDES))
|
||||
|
|
@ -662,10 +787,70 @@ 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_group.nr == 1) {
|
||||
/*
|
||||
* Single remote (the common case): run do_push() directly
|
||||
* in this process. The loop runs exactly once.
|
||||
*
|
||||
* Mirror detection and the --mirror/--all + refspec conflict
|
||||
* checks are done here. rs is rebuilt so that per-remote push
|
||||
* mappings (remote.NAME.push config) are resolved against the
|
||||
* correct remote. inner_flags is a snapshot of flags so that a
|
||||
* mirror remote cannot bleed TRANSPORT_PUSH_FORCE into any
|
||||
* subsequent call.
|
||||
*/
|
||||
base_flags = flags;
|
||||
{
|
||||
int inner_flags = base_flags;
|
||||
struct remote *r = pushremote_get(remote_group.items[0].string);
|
||||
if (!r)
|
||||
die(_("no such remote or remote group: %s"),
|
||||
remote_group.items[0].string);
|
||||
|
||||
if (r->mirror)
|
||||
inner_flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE);
|
||||
|
||||
if (inner_flags & TRANSPORT_PUSH_ALL) {
|
||||
if (argc >= 2)
|
||||
die(_("--all can't be combined with refspecs"));
|
||||
}
|
||||
if (inner_flags & TRANSPORT_PUSH_MIRROR) {
|
||||
if (argc >= 2)
|
||||
die(_("--mirror can't be combined with refspecs"));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
rc = do_push(inner_flags, push_options, r);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Multiple remotes: spawn one "git push <remote> [<refspecs>]"
|
||||
* subprocess per remote, sequentially.
|
||||
*
|
||||
* Options that only make sense for a single transport connection
|
||||
* are rejected here.
|
||||
*/
|
||||
if (flags & TRANSPORT_PUSH_ATOMIC)
|
||||
die(_("--atomic can only be used when pushing to one remote"));
|
||||
|
||||
rc = push_multiple(&remote_group, push_options, flags,
|
||||
tags,
|
||||
argc > 1 ? argv + 1 : NULL,
|
||||
argc > 1 ? argc - 1 : 0);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
37
remote.c
37
remote.c
|
|
@ -2125,6 +2125,43 @@ int get_fetch_map(const struct ref *remote_refs,
|
|||
return 0;
|
||||
}
|
||||
|
||||
int get_remote_group(const char *key, const char *value,
|
||||
const struct config_context *ctx UNUSED,
|
||||
void *priv)
|
||||
{
|
||||
struct remote_group_data *g = priv;
|
||||
|
||||
if (skip_prefix(key, "remotes.", &key) && !strcmp(key, g->name)) {
|
||||
/* split list by white space */
|
||||
while (*value) {
|
||||
size_t wordlen = strcspn(value, " \t\n");
|
||||
|
||||
if (wordlen >= 1)
|
||||
string_list_append_nodup(g->list,
|
||||
xstrndup(value, wordlen));
|
||||
value += wordlen + (value[wordlen] != '\0');
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int add_remote_or_group(const char *name, struct string_list *list)
|
||||
{
|
||||
int prev_nr = list->nr;
|
||||
struct remote_group_data g;
|
||||
g.name = name; g.list = list;
|
||||
|
||||
repo_config(the_repository, get_remote_group, &g);
|
||||
if (list->nr == prev_nr) {
|
||||
struct remote *remote = remote_get(name);
|
||||
if (!remote_is_configured(remote, 0))
|
||||
return 0;
|
||||
string_list_append(list, remote->name);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int resolve_remote_symref(struct ref *ref, struct ref *list)
|
||||
{
|
||||
if (!ref->symref)
|
||||
|
|
|
|||
16
remote.h
16
remote.h
|
|
@ -349,6 +349,18 @@ int branch_has_merge_config(struct branch *branch);
|
|||
|
||||
int branch_merge_matches(struct branch *, int n, const char *);
|
||||
|
||||
/* list of the remote in a group as configured */
|
||||
struct remote_group_data {
|
||||
const char *name;
|
||||
struct string_list *list;
|
||||
};
|
||||
|
||||
int get_remote_group(const char *key, const char *value,
|
||||
const struct config_context *ctx,
|
||||
void *priv);
|
||||
|
||||
int add_remote_or_group(const char *name, struct string_list *list);
|
||||
|
||||
/**
|
||||
* Return the fully-qualified refname of the tracking branch for `branch`.
|
||||
* I.e., what "branch@{upstream}" would give you. Returns NULL if no
|
||||
|
|
@ -420,8 +432,8 @@ struct push_cas_option {
|
|||
unsigned use_tracking:1;
|
||||
char *refname;
|
||||
} *entry;
|
||||
int nr;
|
||||
int alloc;
|
||||
size_t nr;
|
||||
size_t alloc;
|
||||
};
|
||||
|
||||
int parseopt_push_cas_option(const struct option *, const char *arg, int unset);
|
||||
|
|
|
|||
|
|
@ -706,6 +706,7 @@ integration_tests = [
|
|||
't5563-simple-http-auth.sh',
|
||||
't5564-http-proxy.sh',
|
||||
't5565-push-multiple.sh',
|
||||
't5566-push-group.sh',
|
||||
't5570-git-daemon.sh',
|
||||
't5571-pre-push-hook.sh',
|
||||
't5572-pull-submodule.sh',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
#!/bin/sh
|
||||
|
||||
test_description='push to remote group'
|
||||
|
||||
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=default
|
||||
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
test_expect_success 'setup' '
|
||||
for i in 1 2 3
|
||||
do
|
||||
git init --bare dest-$i.git &&
|
||||
git -C dest-$i.git symbolic-ref HEAD refs/heads/not-a-branch ||
|
||||
return 1
|
||||
done &&
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "initial" &&
|
||||
git config set remote.remote-1.url "file://$(pwd)/dest-1.git" &&
|
||||
git config set remote.remote-1.fetch "+refs/heads/*:refs/remotes/remote-1/*" &&
|
||||
git config set remote.remote-2.url "file://$(pwd)/dest-2.git" &&
|
||||
git config set remote.remote-2.fetch "+refs/heads/*:refs/remotes/remote-2/*" &&
|
||||
git config set remote.remote-3.url "file://$(pwd)/dest-3.git" &&
|
||||
git config set remote.remote-3.fetch "+refs/heads/*:refs/remotes/remote-3/*" &&
|
||||
git config set remotes.all-remotes "remote-1 remote-2 remote-3"
|
||||
'
|
||||
|
||||
test_expect_success 'push to remote group updates all members correctly' '
|
||||
git push all-remotes HEAD:refs/heads/main &&
|
||||
git rev-parse HEAD >expect &&
|
||||
for i in 1 2 3
|
||||
do
|
||||
git -C dest-$i.git rev-parse refs/heads/main >actual ||
|
||||
return 1
|
||||
test_cmp expect actual || return 1
|
||||
done
|
||||
'
|
||||
|
||||
test_expect_success 'push second commit to group updates all members' '
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "second" &&
|
||||
git push all-remotes HEAD:refs/heads/main &&
|
||||
git rev-parse HEAD >expect &&
|
||||
for i in 1 2 3
|
||||
do
|
||||
git -C dest-$i.git rev-parse refs/heads/main >actual ||
|
||||
return 1
|
||||
test_cmp expect actual || return 1
|
||||
done
|
||||
'
|
||||
|
||||
test_expect_success 'push to single remote in group does not affect others' '
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "third" &&
|
||||
git push remote-1 HEAD:refs/heads/main &&
|
||||
git -C dest-1.git rev-parse refs/heads/main >hash-after-1 &&
|
||||
git -C dest-2.git rev-parse refs/heads/main >hash-after-2 &&
|
||||
! test_cmp hash-after-1 hash-after-2
|
||||
'
|
||||
|
||||
test_expect_success 'mirror remote in group with refspec fails' '
|
||||
git config set remote.remote-1.mirror true &&
|
||||
test_must_fail git push all-remotes HEAD:refs/heads/main 2>err &&
|
||||
test_grep "mirror" err &&
|
||||
git config unset remote.remote-1.mirror
|
||||
'
|
||||
|
||||
test_expect_success 'push.default=current works with group push' '
|
||||
git config set push.default current &&
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "fifth" &&
|
||||
git push all-remotes &&
|
||||
git config unset push.default
|
||||
'
|
||||
|
||||
test_expect_success '--atomic is rejected for group push' '
|
||||
test_must_fail git push --atomic all-remotes HEAD:refs/heads/main 2>err &&
|
||||
test_grep "atomic" err
|
||||
'
|
||||
|
||||
test_expect_success 'push continues past rejection to remaining remotes' '
|
||||
for i in c1 c2 c3
|
||||
do
|
||||
git init --bare dest-$i.git || return 1
|
||||
done &&
|
||||
git config set remote.c1.url "file://$(pwd)/dest-c1.git" &&
|
||||
git config set remote.c2.url "file://$(pwd)/dest-c2.git" &&
|
||||
git config set remote.c3.url "file://$(pwd)/dest-c3.git" &&
|
||||
git config set remotes.continue-group "c1 c2 c3" &&
|
||||
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "base for continue test" &&
|
||||
|
||||
# initial sync
|
||||
git push continue-group HEAD:refs/heads/main &&
|
||||
|
||||
# advance c2 independently
|
||||
git clone dest-c2.git tmp-c2 &&
|
||||
(
|
||||
cd tmp-c2 &&
|
||||
git checkout -b main origin/main &&
|
||||
test_commit c2_independent &&
|
||||
git push origin HEAD:refs/heads/main
|
||||
) &&
|
||||
rm -rf tmp-c2 &&
|
||||
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "local diverging commit" &&
|
||||
|
||||
# push: c2 rejects, others succeed
|
||||
test_must_fail git push continue-group HEAD:refs/heads/main &&
|
||||
|
||||
git rev-parse HEAD >expect &&
|
||||
git -C dest-c1.git rev-parse refs/heads/main >actual-c1 &&
|
||||
git -C dest-c3.git rev-parse refs/heads/main >actual-c3 &&
|
||||
test_cmp expect actual-c1 &&
|
||||
test_cmp expect actual-c3 &&
|
||||
|
||||
# c2 should not have the new commit
|
||||
git -C dest-c2.git rev-parse refs/heads/main >actual-c2 &&
|
||||
! test_cmp expect actual-c2
|
||||
'
|
||||
|
||||
test_expect_success 'fatal connection error does not stop remaining remotes' '
|
||||
for i in f1 f2 f3
|
||||
do
|
||||
git init --bare dest-$i.git || return 1
|
||||
done &&
|
||||
git config set remote.f1.url "file://$(pwd)/dest-f1.git" &&
|
||||
git config set remote.f2.url "file://$(pwd)/dest-f2.git" &&
|
||||
git config set remote.f3.url "file://$(pwd)/dest-f3.git" &&
|
||||
git config set remotes.fatal-group "f1 f2 f3" &&
|
||||
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "base for fatal test" &&
|
||||
|
||||
# initial sync
|
||||
git push fatal-group HEAD:refs/heads/main &&
|
||||
|
||||
# break f2
|
||||
git config set remote.f2.url "file:///tmp/does-not-exist-$$" &&
|
||||
|
||||
test_tick &&
|
||||
git commit --allow-empty -m "after fatal setup" &&
|
||||
|
||||
# overall exit code is non-zero because f2 failed
|
||||
test_must_fail git push fatal-group HEAD:refs/heads/main &&
|
||||
|
||||
git rev-parse HEAD >expect &&
|
||||
|
||||
# f1 and f3 should both have the new commit — subprocesses are independent
|
||||
git -C dest-f1.git rev-parse refs/heads/main >actual-f1 &&
|
||||
test_cmp expect actual-f1 &&
|
||||
git -C dest-f3.git rev-parse refs/heads/main >actual-f3 &&
|
||||
test_cmp expect actual-f3 &&
|
||||
|
||||
git config set remote.f2.url "file://$(pwd)/dest-f2.git"
|
||||
'
|
||||
|
||||
test_done
|
||||
Loading…
Reference in New Issue