aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/config.txt5
-rw-r--r--Documentation/git-push.txt7
-rw-r--r--Documentation/git-send-pack.txt7
-rw-r--r--Documentation/technical/protocol-capabilities.txt13
-rw-r--r--builtin/push.c5
-rw-r--r--builtin/receive-pack.c165
-rw-r--r--builtin/send-pack.c6
-rw-r--r--remote.h3
-rw-r--r--send-pack.c65
-rw-r--r--send-pack.h3
-rwxr-xr-xt/t5543-atomic-push.sh194
-rw-r--r--transport.c5
-rw-r--r--transport.h1
13 files changed, 429 insertions, 50 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 04e2a7168..1a54eae8f 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2094,6 +2094,11 @@ rebase.autostash::
successful rebase might result in non-trivial conflicts.
Defaults to false.
+receive.advertiseatomic::
+ By default, git-receive-pack will advertise the atomic push
+ capability to its clients. If you don't want to this capability
+ to be advertised, set this variable to false.
+
receive.autogc::
By default, git-receive-pack will run "git-gc --auto" after
receiving data from git-push and updating refs. You can stop
diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
index b17283ab7..ea9757692 100644
--- a/Documentation/git-push.txt
+++ b/Documentation/git-push.txt
@@ -9,7 +9,7 @@ git-push - Update remote refs along with associated objects
SYNOPSIS
--------
[verse]
-'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
+'git push' [--all | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
[--repo=<repository>] [-f | --force] [--prune] [-v | --verbose]
[-u | --set-upstream] [--signed]
[--force-with-lease[=<refname>[:<expect>]]]
@@ -136,6 +136,11 @@ already exists on the remote side.
logged. See linkgit:git-receive-pack[1] for the details
on the receiving end.
+--[no-]atomic::
+ Use an atomic transaction on the remote side if available.
+ Either all refs are updated, or on error, no refs are updated.
+ If the server does not support atomic pushes the push will fail.
+
--receive-pack=<git-receive-pack>::
--exec=<git-receive-pack>::
Path to the 'git-receive-pack' program on the remote
diff --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt
index 2a0de42a7..45c7725dc 100644
--- a/Documentation/git-send-pack.txt
+++ b/Documentation/git-send-pack.txt
@@ -9,7 +9,7 @@ git-send-pack - Push objects over Git protocol to another repository
SYNOPSIS
--------
[verse]
-'git send-pack' [--all] [--dry-run] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [<host>:]<directory> [<ref>...]
+'git send-pack' [--all] [--dry-run] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [--atomic] [<host>:]<directory> [<ref>...]
DESCRIPTION
-----------
@@ -62,6 +62,11 @@ be in a separate packet, and the list must end with a flush packet.
Send a "thin" pack, which records objects in deltified form based
on objects not included in the pack to reduce network traffic.
+--atomic::
+ Use an atomic transaction for updating the refs. If any of the refs
+ fails to update then the entire push will fail without changing any
+ refs.
+
<host>::
A remote host to house the repository. When this
part is specified, 'git-receive-pack' is invoked via
diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 6d5424c1b..4f8a7bfb4 100644
--- a/Documentation/technical/protocol-capabilities.txt
+++ b/Documentation/technical/protocol-capabilities.txt
@@ -18,8 +18,9 @@ was sent. Server MUST NOT ignore capabilities that client requested
and server advertised. As a consequence of these rules, server MUST
NOT advertise capabilities it does not understand.
-The 'report-status', 'delete-refs', 'quiet', and 'push-cert' capabilities
-are sent and recognized by the receive-pack (push to server) process.
+The 'atomic', 'report-status', 'delete-refs', 'quiet', and 'push-cert'
+capabilities are sent and recognized by the receive-pack (push to server)
+process.
The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized
by both upload-pack and receive-pack protocols. The 'agent' capability
@@ -244,6 +245,14 @@ respond with the 'quiet' capability to suppress server-side progress
reporting if the local progress reporting is also being suppressed
(e.g., via `push -q`, or if stderr does not go to a tty).
+atomic
+------
+
+If the server sends the 'atomic' capability it is capable of accepting
+atomic pushes. If the pushing client requests this capability, the server
+will update the refs in one atomic transaction. Either all refs are
+updated or none.
+
allow-tip-sha1-in-want
----------------------
diff --git a/builtin/push.c b/builtin/push.c
index 12f5e6939..fc771a9f6 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -487,6 +487,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
int flags = 0;
int tags = 0;
int rc;
+ int atomic = 0;
const char *repo = NULL; /* default repository */
struct option options[] = {
OPT__VERBOSITY(&verbosity),
@@ -518,6 +519,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"),
TRANSPORT_PUSH_FOLLOW_TAGS),
OPT_BIT(0, "signed", &flags, N_("GPG sign the push"), TRANSPORT_PUSH_CERT),
+ OPT_BOOL(0, "atomic", &atomic, N_("request atomic transaction on remote side")),
OPT_END()
};
@@ -533,6 +535,9 @@ int cmd_push(int argc, const char **argv, const char *prefix)
if (tags)
add_refspec("refs/tags/*");
+ if (atomic)
+ flags |= TRANSPORT_PUSH_ATOMIC;
+
if (argc > 0) {
repo = argv[0];
set_refspecs(argv + 1, argc - 1, repo);
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 8266c1fcc..4e85e25d0 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -38,9 +38,11 @@ static int receive_fsck_objects = -1;
static int transfer_fsck_objects = -1;
static int receive_unpack_limit = -1;
static int transfer_unpack_limit = -1;
+static int advertise_atomic_push = 1;
static int unpack_limit = 100;
static int report_status;
static int use_sideband;
+static int use_atomic;
static int quiet;
static int prefer_ofs_delta = 1;
static int auto_update_server_info;
@@ -67,6 +69,7 @@ static const char *NONCE_SLOP = "SLOP";
static const char *nonce_status;
static long nonce_stamp_slop;
static unsigned long nonce_stamp_slop_limit;
+static struct ref_transaction *transaction;
static enum deny_action parse_deny_action(const char *var, const char *value)
{
@@ -160,6 +163,11 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
return 0;
}
+ if (strcmp(var, "receive.advertiseatomic") == 0) {
+ advertise_atomic_push = git_config_bool(var, value);
+ return 0;
+ }
+
return git_default_config(var, value, cb);
}
@@ -175,6 +183,8 @@ static void show_ref(const char *path, const unsigned char *sha1)
strbuf_addstr(&cap,
"report-status delete-refs side-band-64k quiet");
+ if (advertise_atomic_push)
+ strbuf_addstr(&cap, " atomic");
if (prefer_ofs_delta)
strbuf_addstr(&cap, " ofs-delta");
if (push_cert_nonce)
@@ -910,6 +920,7 @@ static const char *update(struct command *cmd, struct shallow_info *si)
}
if (is_null_sha1(new_sha1)) {
+ struct strbuf err = STRBUF_INIT;
if (!parse_object(old_sha1)) {
old_sha1 = NULL;
if (ref_exists(name)) {
@@ -919,35 +930,36 @@ static const char *update(struct command *cmd, struct shallow_info *si)
cmd->did_not_exist = 1;
}
}
- if (delete_ref(namespaced_name, old_sha1, 0)) {
- rp_error("failed to delete %s", name);
+ if (ref_transaction_delete(transaction,
+ namespaced_name,
+ old_sha1,
+ 0, old_sha1 != NULL,
+ "push", &err)) {
+ rp_error("%s", err.buf);
+ strbuf_release(&err);
return "failed to delete";
}
+ strbuf_release(&err);
return NULL; /* good */
}
else {
struct strbuf err = STRBUF_INIT;
- struct ref_transaction *transaction;
-
if (shallow_update && si->shallow_ref[cmd->index] &&
update_shallow_ref(cmd, si))
return "shallow error";
- transaction = ref_transaction_begin(&err);
- if (!transaction ||
- ref_transaction_update(transaction, namespaced_name,
- new_sha1, old_sha1, 0, 1, "push",
- &err) ||
- ref_transaction_commit(transaction, &err)) {
- ref_transaction_free(transaction);
-
+ if (ref_transaction_update(transaction,
+ namespaced_name,
+ new_sha1, old_sha1,
+ 0, 1, "push",
+ &err)) {
rp_error("%s", err.buf);
strbuf_release(&err);
+
return "failed to update ref";
}
-
- ref_transaction_free(transaction);
strbuf_release(&err);
+
return NULL; /* good */
}
}
@@ -1131,11 +1143,105 @@ static void reject_updates_to_hidden(struct command *commands)
}
}
+static int should_process_cmd(struct command *cmd)
+{
+ return !cmd->error_string && !cmd->skip_update;
+}
+
+static void warn_if_skipped_connectivity_check(struct command *commands,
+ struct shallow_info *si)
+{
+ struct command *cmd;
+ int checked_connectivity = 1;
+
+ for (cmd = commands; cmd; cmd = cmd->next) {
+ if (should_process_cmd(cmd) && si->shallow_ref[cmd->index]) {
+ error("BUG: connectivity check has not been run on ref %s",
+ cmd->ref_name);
+ checked_connectivity = 0;
+ }
+ }
+ if (!checked_connectivity)
+ die("BUG: connectivity check skipped???");
+}
+
+static void execute_commands_non_atomic(struct command *commands,
+ struct shallow_info *si)
+{
+ struct command *cmd;
+ struct strbuf err = STRBUF_INIT;
+
+ for (cmd = commands; cmd; cmd = cmd->next) {
+ if (!should_process_cmd(cmd))
+ continue;
+
+ transaction = ref_transaction_begin(&err);
+ if (!transaction) {
+ rp_error("%s", err.buf);
+ strbuf_reset(&err);
+ cmd->error_string = "transaction failed to start";
+ continue;
+ }
+
+ cmd->error_string = update(cmd, si);
+
+ if (!cmd->error_string
+ && ref_transaction_commit(transaction, &err)) {
+ rp_error("%s", err.buf);
+ strbuf_reset(&err);
+ cmd->error_string = "failed to update ref";
+ }
+ ref_transaction_free(transaction);
+ }
+ strbuf_release(&err);
+}
+
+static void execute_commands_atomic(struct command *commands,
+ struct shallow_info *si)
+{
+ struct command *cmd;
+ struct strbuf err = STRBUF_INIT;
+ const char *reported_error = "atomic push failure";
+
+ transaction = ref_transaction_begin(&err);
+ if (!transaction) {
+ rp_error("%s", err.buf);
+ strbuf_reset(&err);
+ reported_error = "transaction failed to start";
+ goto failure;
+ }
+
+ for (cmd = commands; cmd; cmd = cmd->next) {
+ if (!should_process_cmd(cmd))
+ continue;
+
+ cmd->error_string = update(cmd, si);
+
+ if (cmd->error_string)
+ goto failure;
+ }
+
+ if (ref_transaction_commit(transaction, &err)) {
+ rp_error("%s", err.buf);
+ reported_error = "atomic transaction failed";
+ goto failure;
+ }
+ goto cleanup;
+
+failure:
+ for (cmd = commands; cmd; cmd = cmd->next)
+ if (!cmd->error_string)
+ cmd->error_string = reported_error;
+
+cleanup:
+ ref_transaction_free(transaction);
+ strbuf_release(&err);
+}
+
static void execute_commands(struct command *commands,
const char *unpacker_error,
struct shallow_info *si)
{
- int checked_connectivity;
struct command *cmd;
unsigned char sha1[20];
struct iterate_data data;
@@ -1166,27 +1272,13 @@ static void execute_commands(struct command *commands,
free(head_name_to_free);
head_name = head_name_to_free = resolve_refdup("HEAD", 0, sha1, NULL);
- checked_connectivity = 1;
- for (cmd = commands; cmd; cmd = cmd->next) {
- if (cmd->error_string)
- continue;
-
- if (cmd->skip_update)
- continue;
-
- cmd->error_string = update(cmd, si);
- if (shallow_update && !cmd->error_string &&
- si->shallow_ref[cmd->index]) {
- error("BUG: connectivity check has not been run on ref %s",
- cmd->ref_name);
- checked_connectivity = 0;
- }
- }
+ if (use_atomic)
+ execute_commands_atomic(commands, si);
+ else
+ execute_commands_non_atomic(commands, si);
- if (shallow_update && !checked_connectivity)
- error("BUG: run 'git fsck' for safety.\n"
- "If there are errors, try to remove "
- "the reported refs above");
+ if (shallow_update)
+ warn_if_skipped_connectivity_check(commands, si);
}
static struct command **queue_command(struct command **tail,
@@ -1268,6 +1360,9 @@ static struct command *read_head_info(struct sha1_array *shallow)
use_sideband = LARGE_PACKET_MAX;
if (parse_feature_request(feature_list, "quiet"))
quiet = 1;
+ if (advertise_atomic_push
+ && parse_feature_request(feature_list, "atomic"))
+ use_atomic = 1;
}
if (!strcmp(line, "push-cert")) {
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index b564a7784..b961e5ae7 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -13,7 +13,7 @@
#include "sha1-array.h"
static const char send_pack_usage[] =
-"git send-pack [--all | --mirror] [--dry-run] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [<host>:]<directory> [<ref>...]\n"
+"git send-pack [--all | --mirror] [--dry-run] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [--atomic] [<host>:]<directory> [<ref>...]\n"
" --all and explicit <ref> specification are mutually exclusive.";
static struct send_pack_args args;
@@ -170,6 +170,10 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
args.use_thin_pack = 1;
continue;
}
+ if (!strcmp(arg, "--atomic")) {
+ args.atomic = 1;
+ continue;
+ }
if (!strcmp(arg, "--stateless-rpc")) {
args.stateless_rpc = 1;
continue;
diff --git a/remote.h b/remote.h
index 8b62efd2a..f346524db 100644
--- a/remote.h
+++ b/remote.h
@@ -115,7 +115,8 @@ struct ref {
REF_STATUS_REJECT_SHALLOW,
REF_STATUS_UPTODATE,
REF_STATUS_REMOTE_REJECT,
- REF_STATUS_EXPECTING_REPORT
+ REF_STATUS_EXPECTING_REPORT,
+ REF_STATUS_ATOMIC_PUSH_FAILED
} status;
char *remote_status;
struct ref *peer_ref; /* when renaming */
diff --git a/send-pack.c b/send-pack.c
index 25947d7df..9d2b0c52e 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -193,10 +193,13 @@ static void advertise_shallow_grafts_buf(struct strbuf *sb)
for_each_commit_graft(advertise_shallow_grafts_cb, sb);
}
-static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args)
+#define CHECK_REF_NO_PUSH -1
+#define CHECK_REF_STATUS_REJECTED -2
+#define CHECK_REF_UPTODATE -3
+static int check_to_send_update(const struct ref *ref, const struct send_pack_args *args)
{
if (!ref->peer_ref && !args->send_mirror)
- return 0;
+ return CHECK_REF_NO_PUSH;
/* Check for statuses set by set_ref_status_for_push() */
switch (ref->status) {
@@ -206,10 +209,11 @@ static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_a
case REF_STATUS_REJECT_NEEDS_FORCE:
case REF_STATUS_REJECT_STALE:
case REF_STATUS_REJECT_NODELETE:
+ return CHECK_REF_STATUS_REJECTED;
case REF_STATUS_UPTODATE:
- return 0;
+ return CHECK_REF_UPTODATE;
default:
- return 1;
+ return 0;
}
}
@@ -253,7 +257,7 @@ static int generate_push_cert(struct strbuf *req_buf,
strbuf_addstr(&cert, "\n");
for (ref = remote_refs; ref; ref = ref->next) {
- if (!ref_update_to_be_sent(ref, args))
+ if (check_to_send_update(ref, args) < 0)
continue;
update_seen = 1;
strbuf_addf(&cert, "%s %s %s\n",
@@ -281,6 +285,29 @@ free_return:
return update_seen;
}
+
+static int atomic_push_failure(struct send_pack_args *args,
+ struct ref *remote_refs,
+ struct ref *failing_ref)
+{
+ struct ref *ref;
+ /* Mark other refs as failed */
+ for (ref = remote_refs; ref; ref = ref->next) {
+ if (!ref->peer_ref && !args->send_mirror)
+ continue;
+
+ switch (ref->status) {
+ case REF_STATUS_EXPECTING_REPORT:
+ ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+ continue;
+ default:
+ break; /* do nothing */
+ }
+ }
+ return error("atomic push failed for ref %s. status: %d\n",
+ failing_ref->name, failing_ref->status);
+}
+
int send_pack(struct send_pack_args *args,
int fd[], struct child_process *conn,
struct ref *remote_refs,
@@ -297,6 +324,8 @@ int send_pack(struct send_pack_args *args,
int use_sideband = 0;
int quiet_supported = 0;
int agent_supported = 0;
+ int use_atomic = 0;
+ int atomic_supported = 0;
unsigned cmds_sent = 0;
int ret;
struct async demux;
@@ -317,6 +346,8 @@ int send_pack(struct send_pack_args *args,
agent_supported = 1;
if (server_supports("no-thin"))
args->use_thin_pack = 0;
+ if (server_supports("atomic"))
+ atomic_supported = 1;
if (args->push_cert) {
int len;
@@ -331,6 +362,10 @@ int send_pack(struct send_pack_args *args,
"Perhaps you should specify a branch such as 'master'.\n");
return 0;
}
+ if (args->atomic && !atomic_supported)
+ die(_("server does not support --atomic push"));
+
+ use_atomic = atomic_supported && args->atomic;
if (status_report)
strbuf_addstr(&cap_buf, " report-status");
@@ -338,6 +373,8 @@ int send_pack(struct send_pack_args *args,
strbuf_addstr(&cap_buf, " side-band-64k");
if (quiet_supported && (args->quiet || !args->progress))
strbuf_addstr(&cap_buf, " quiet");
+ if (use_atomic)
+ strbuf_addstr(&cap_buf, " atomic");
if (agent_supported)
strbuf_addf(&cap_buf, " agent=%s", git_user_agent_sanitized());
@@ -362,9 +399,21 @@ int send_pack(struct send_pack_args *args,
* the pack data.
*/
for (ref = remote_refs; ref; ref = ref->next) {
- if (!ref_update_to_be_sent(ref, args))
+ switch (check_to_send_update(ref, args)) {
+ case 0: /* no error */
+ break;
+ case CHECK_REF_STATUS_REJECTED:
+ /*
+ * When we know the server would reject a ref update if
+ * we were to send it and we're trying to send the refs
+ * atomically, abort the whole operation.
+ */
+ if (use_atomic)
+ return atomic_push_failure(args, remote_refs, ref);
+ /* Fallthrough for non atomic case. */
+ default:
continue;
-
+ }
if (!ref->deletion)
need_pack_data = 1;
@@ -383,7 +432,7 @@ int send_pack(struct send_pack_args *args,
if (args->dry_run || args->push_cert)
continue;
- if (!ref_update_to_be_sent(ref, args))
+ if (check_to_send_update(ref, args) < 0)
continue;
old_hex = sha1_to_hex(ref->old_sha1);
diff --git a/send-pack.h b/send-pack.h
index 563545774..b6646488a 100644
--- a/send-pack.h
+++ b/send-pack.h
@@ -13,7 +13,8 @@ struct send_pack_args {
use_ofs_delta:1,
dry_run:1,
push_cert:1,
- stateless_rpc:1;
+ stateless_rpc:1,
+ atomic:1;
};
int send_pack(struct send_pack_args *args,
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
new file mode 100755
index 000000000..3480b3300
--- /dev/null
+++ b/t/t5543-atomic-push.sh
@@ -0,0 +1,194 @@
+#!/bin/sh
+
+test_description='pushing to a repository using the atomic push option'
+
+. ./test-lib.sh
+
+mk_repo_pair () {
+ rm -rf workbench upstream &&
+ test_create_repo upstream &&
+ test_create_repo workbench &&
+ (
+ cd upstream &&
+ git config receive.denyCurrentBranch warn
+ ) &&
+ (
+ cd workbench &&
+ git remote add up ../upstream
+ )
+}
+
+# Compare the ref ($1) in upstream with a ref value from workbench ($2)
+# i.e. test_refs second HEAD@{2}
+test_refs () {
+ test $# = 2 &&
+ git -C upstream rev-parse --verify "$1" >expect &&
+ git -C workbench rev-parse --verify "$2" >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success 'atomic push works for a single branch' '
+ mk_repo_pair &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git push --mirror up &&
+ test_commit two &&
+ git push --atomic up master
+ ) &&
+ test_refs master master
+'
+
+test_expect_success 'atomic push works for two branches' '
+ mk_repo_pair &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git branch second &&
+ git push --mirror up &&
+ test_commit two &&
+ git checkout second &&
+ test_commit three &&
+ git push --atomic up master second
+ ) &&
+ test_refs master master &&
+ test_refs second second
+'
+
+test_expect_success 'atomic push works in combination with --mirror' '
+ mk_repo_pair &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git checkout -b second &&
+ test_commit two &&
+ git push --atomic --mirror up
+ ) &&
+ test_refs master master &&
+ test_refs second second
+'
+
+test_expect_success 'atomic push works in combination with --force' '
+ mk_repo_pair &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git branch second master &&
+ test_commit two_a &&
+ git checkout second &&
+ test_commit two_b &&
+ test_commit three_b &&
+ test_commit four &&
+ git push --mirror up &&
+ # The actual test is below
+ git checkout master &&
+ test_commit three_a &&
+ git checkout second &&
+ git reset --hard HEAD^ &&
+ git push --force --atomic up master second
+ ) &&
+ test_refs master master &&
+ test_refs second second
+'
+
+# set up two branches where master can be pushed but second can not
+# (non-fast-forward). Since second can not be pushed the whole operation
+# will fail and leave master untouched.
+test_expect_success 'atomic push fails if one branch fails' '
+ mk_repo_pair &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git checkout -b second master &&
+ test_commit two &&
+ test_commit three &&
+ test_commit four &&
+ git push --mirror up &&
+ git reset --hard HEAD~2 &&
+ test_commit five &&
+ git checkout master &&
+ test_commit six &&
+ test_must_fail git push --atomic --all up
+ ) &&
+ test_refs master HEAD@{7} &&
+ test_refs second HEAD@{4}
+'
+
+test_expect_success 'atomic push fails if one tag fails remotely' '
+ # prepare the repo
+ mk_repo_pair &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git checkout -b second master &&
+ test_commit two &&
+ git push --mirror up
+ ) &&
+ # a third party modifies the server side:
+ (
+ cd upstream &&
+ git checkout second &&
+ git tag test_tag second
+ ) &&
+ # see if we can now push both branches.
+ (
+ cd workbench &&
+ git checkout master &&
+ test_commit three &&
+ git checkout second &&
+ test_commit four &&
+ git tag test_tag &&
+ test_must_fail git push --tags --atomic up master second
+ ) &&
+ test_refs master HEAD@{3} &&
+ test_refs second HEAD@{1}
+'
+
+test_expect_success 'atomic push obeys update hook preventing a branch to be pushed' '
+ mk_repo_pair &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git checkout -b second master &&
+ test_commit two &&
+ git push --mirror up
+ ) &&
+ (
+ cd upstream &&
+ HOOKDIR="$(git rev-parse --git-dir)/hooks" &&
+ HOOK="$HOOKDIR/update" &&
+ mkdir -p "$HOOKDIR" &&
+ write_script "$HOOK" <<-\EOF
+ # only allow update to master from now on
+ test "$1" = "refs/heads/master"
+ EOF
+ ) &&
+ (
+ cd workbench &&
+ git checkout master &&
+ test_commit three &&
+ git checkout second &&
+ test_commit four &&
+ test_must_fail git push --atomic up master second
+ ) &&
+ test_refs master HEAD@{3} &&
+ test_refs second HEAD@{1}
+'
+
+test_expect_success 'atomic push is not advertised if configured' '
+ mk_repo_pair &&
+ (
+ cd upstream
+ git config receive.advertiseatomic 0
+ ) &&
+ (
+ cd workbench &&
+ test_commit one &&
+ git push --mirror up &&
+ test_commit two &&
+ test_must_fail git push --atomic up master
+ ) &&
+ test_refs master HEAD@{1}
+'
+
+test_done
diff --git a/transport.c b/transport.c
index 08bcd3a4e..0694a7cf3 100644
--- a/transport.c
+++ b/transport.c
@@ -728,6 +728,10 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count, i
ref->deletion ? NULL : ref->peer_ref,
"remote failed to report status", porcelain);
break;
+ case REF_STATUS_ATOMIC_PUSH_FAILED:
+ print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+ "atomic push failed", porcelain);
+ break;
case REF_STATUS_OK:
print_ok_ref_status(ref, porcelain);
break;
@@ -826,6 +830,7 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re
args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN);
args.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN);
args.push_cert = !!(flags & TRANSPORT_PUSH_CERT);
+ args.atomic = !!(flags & TRANSPORT_PUSH_ATOMIC);
args.url = transport->url;
ret = send_pack(&args, data->fd, data->conn, remote_refs,
diff --git a/transport.h b/transport.h
index 3e0091eaa..18d2cf827 100644
--- a/transport.h
+++ b/transport.h
@@ -125,6 +125,7 @@ struct transport {
#define TRANSPORT_PUSH_NO_HOOK 512
#define TRANSPORT_PUSH_FOLLOW_TAGS 1024
#define TRANSPORT_PUSH_CERT 2048
+#define TRANSPORT_PUSH_ATOMIC 4096
#define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
#define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)