threads / patch / 38179

patch, 5 partsAdd a flag to push atomically

Subject: [PATCH 0/5] Add a flag to push atomically

## tl;dr

56 messages between Dec 15, 2014 and Dec 19, 2014. Diffs are folded; open one to read it.

replies: 55people: 3as markdown or json

Stefan Beller· Dec 15, 2014, 19:56 UTC · lore
This patch series adds a flag to git push to update the remote refs atomically.

It was part of a longer patch series[1]. This series applies on top of origin/mh/reflog-expire It can also be found at [2].

Change since picking the series up from Ronnie.
  * other anchor point (i.e. where the series applies)
  * more tests for this feature, specially testing failures
  * drop the patch to rename ref_transaction_* to transaction_*_ref
  * slight rewording of the additional documentation

[1] http://www.spinics.net/lists/git/msg241214.html [2] https://github.com/stefanbeller/git/tree/atomic-push-v1

Ronnie Sahlberg (4):
  receive-pack.c: add protocol support to negotiate atomic-push
  send-pack.c: add an --atomic-push command line argument
  receive-pack.c: use a single transaction when atomic-push is
    negotiated
  push.c: add an --atomic-push argument
Stefan Beller (1):
  t5543-atomic-push.sh: add basic tests for atomic pushes
 Documentation/git-push.txt                        |   8 +-
 Documentation/git-send-pack.txt                   |   7 +-
 Documentation/technical/protocol-capabilities.txt |  12 +-
 builtin/push.c                                    |   2 +
 builtin/receive-pack.c                            |  79 +++++++--
 builtin/send-pack.c                               |   6 +-
 remote.h                                          |   3 +-
 send-pack.c                                       |  45 +++++-
 send-pack.h                                       |   1 +
 t/t5543-atomic-push.sh                            | 185 ++++++++++++++++++++++
 transport.c                                       |   5 +
 transport.h                                       |   1 +
 12 files changed, 327 insertions(+), 27 deletions(-)
 create mode 100755 t/t5543-atomic-push.sh
-- 
2.2.0.33.gc2219e3.dirty
Stefan Beller· Dec 15, 2014, 19:56 UTC · re: Stefan Beller · lore

[PATCH 1/5] receive-pack.c: add protocol support to negotiate atomic-push

From: Ronnie Sahlberg <sahlberg@google.com>
This adds support to the protocol between send-pack and receive-pack to
* allow receive-pack to inform the client that it has atomic push capability
* allow send-pack to request atomic push back.

There is currently no setting in send-pack to actually request that atomic pushes are to be used yet. This only adds protocol capability not ability for the user to activate it.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
 Documentation/technical/protocol-capabilities.txt | 12 ++++++++++--
 builtin/receive-pack.c                            |  6 +++++-
 send-pack.c                                       |  6 ++++++
 3 files changed, 21 insertions(+), 3 deletions(-)
Show changes to 3 files +21 −3

Documentation/technical/protocol-capabilities.txt, builtin/receive-pack.c, send-pack.c

diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 6d5424c..763120c 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-push', '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,13 @@ 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-push
+-----------
+
+If the server sends the 'atomic-push' capability, it means it is
+capable of accepting atomic pushes. If the pushing client requests this
+capability, the server will update the refs in one single atomic transaction.
+
 allow-tip-sha1-in-want
 ----------------------
 
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 32fc540..0c642ab 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -40,6 +40,7 @@ static int transfer_unpack_limit = -1;
 static int unpack_limit = 100;
 static int report_status;
 static int use_sideband;
+static int use_atomic_push;
 static int quiet;
 static int prefer_ofs_delta = 1;
 static int auto_update_server_info;
@@ -171,7 +172,8 @@ static void show_ref(const char *path, const unsigned char *sha1)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status delete-refs side-band-64k quiet "
+			      "atomic-push");
 		if (prefer_ofs_delta)
 			strbuf_addstr(&cap, " ofs-delta");
 		if (push_cert_nonce)
@@ -1179,6 +1181,8 @@ 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 (parse_feature_request(feature_list, "atomic-push"))
+				use_atomic_push = 1;
 		}
 
 		if (!strcmp(line, "push-cert")) {
diff --git a/send-pack.c b/send-pack.c
index 949cb61..1ccc84c 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -294,6 +294,8 @@ int send_pack(struct send_pack_args *args,
 	int use_sideband = 0;
 	int quiet_supported = 0;
 	int agent_supported = 0;
+	int atomic_push_supported = 0;
+	int atomic_push = 0;
 	unsigned cmds_sent = 0;
 	int ret;
 	struct async demux;
@@ -314,6 +316,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-push"))
+		atomic_push_supported = 1;
 	if (args->push_cert) {
 		int len;
 
@@ -335,6 +339,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 (atomic_push)
+		strbuf_addstr(&cap_buf, " atomic-push");
 	if (agent_supported)
 		strbuf_addf(&cap_buf, " agent=%s", git_user_agent_sanitized());
 
-- 
2.2.0.33.gc2219e3.dirty
Junio C Hamano· Dec 15, 2014, 20:53 UTC · re: Stefan Beller · lore

Re: [PATCH 1/5] receive-pack.c: add protocol support to negotiate atomic-push

Stefan Beller <sbeller@google.com> writes:
Show 9 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> This adds support to the protocol between send-pack and receive-pack to
> * allow receive-pack to inform the client that it has atomic push capability
> * allow send-pack to request atomic push back.
>
> There is currently no setting in send-pack to actually request that atomic
> pushes are to be used yet. This only adds protocol capability not ability
> for the user to activate it.
Hmph, am I reading the patch to send-pack.c correctly?

It detects if the other side supports the capability and leaves it in atomic_push_supported variable for later use, and also requests the feature to be activated when atomic_push is set, but I see no logic to link these two together, e.g. error out when atomic_push is true and atomic_push_supported is false (or turn it off with a warning, or whatever).

Show 30 quoted lines
> diff --git a/send-pack.c b/send-pack.c
> index 949cb61..1ccc84c 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -294,6 +294,8 @@ int send_pack(struct send_pack_args *args,
>  	int use_sideband = 0;
>  	int quiet_supported = 0;
>  	int agent_supported = 0;
> +	int atomic_push_supported = 0;
> +	int atomic_push = 0;
>  	unsigned cmds_sent = 0;
>  	int ret;
>  	struct async demux;
> @@ -314,6 +316,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-push"))
> +		atomic_push_supported = 1;
>  	if (args->push_cert) {
>  		int len;
>  
> @@ -335,6 +339,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 (atomic_push)
> +		strbuf_addstr(&cap_buf, " atomic-push");
>  	if (agent_supported)
>  		strbuf_addf(&cap_buf, " agent=%s", git_user_agent_sanitized());
Stefan Beller· Dec 15, 2014, 22:30 UTC · re: Junio C Hamano · lore

Re: [PATCH 1/5] receive-pack.c: add protocol support to negotiate atomic-push

On Mon, Dec 15, 2014 at 12:53 PM, Junio C Hamano <gitster@pobox.com> wrote:
Show 8 quoted lines
> Hmph, am I reading the patch to send-pack.c correctly?
>
> It detects if the other side supports the capability and leaves it
> in atomic_push_supported variable for later use, and also requests
> the feature to be activated when atomic_push is set, but I see no
> logic to link these two together, e.g. error out when atomic_push
> is true and atomic_push_supported is false (or turn it off with a
> warning, or whatever).
This is what you mean by
Show 7 quoted lines
>
>> +     if (args->use_atomic_push && !atomic_push_supported) {
>> +             fprintf(stderr, "Server does not support atomic-push.");
>> +             return -1;
>> +     }
>
> This check logically belongs to the previous step, no?
from the next patch? If so it will be part of the next reroll.

Thanks, Stefan

Stefan Beller· Dec 15, 2014, 19:56 UTC · re: Stefan Beller · lore

[PATCH 2/5] send-pack.c: add an --atomic-push command line argument

From: Ronnie Sahlberg <sahlberg@google.com>

This adds support to send-pack to negotiate and use atomic pushes iff the server supports it. Atomic pushes are activated by a new command line flag --atomic-push.

In order to do this we also need to change the semantics for send_pack() slightly. The existing send_pack() function actually doesn't send all the refs back to the server when multiple refs are involved, for example when using --all. Several of the failure modes for pushes can already be detected locally in the send_pack client based on the information from the initial server side list of all the refs as generated by receive-pack. Any such refs that we thus know would fail to push are thus pruned from the list of refs we send to the server to update.

For atomic pushes, we have to deal thus with both failures that are detected locally as well as failures that are reported back from the server. In order to do so we treat all local failures as push failures too.

We introduce a new status code REF_STATUS_ATOMIC_PUSH_FAILED so we can flag all refs that we would normally have tried to push to the server but we did not due to local failures. This is to improve the error message back to the end user to flag that "these refs failed to update since the atomic push operation failed."

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
 Documentation/git-send-pack.txt |  7 ++++++-
 builtin/send-pack.c             |  6 +++++-
 remote.h                        |  3 ++-
 send-pack.c                     | 39 ++++++++++++++++++++++++++++++++++-----
 send-pack.h                     |  1 +
 transport.c                     |  4 ++++
 6 files changed, 52 insertions(+), 8 deletions(-)
Show changes to 6 files +52 −8

Documentation/git-send-pack.txt, builtin/send-pack.c, remote.h, send-pack.c, send-pack.h, transport.c

diff --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt
index 2a0de42..9296587 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-push] [<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-push::
+	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/builtin/send-pack.c b/builtin/send-pack.c
index b564a77..93cb17c 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-push] [<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-push")) {
+				args.use_atomic_push = 1;
+				continue;
+			}
 			if (!strcmp(arg, "--stateless-rpc")) {
 				args.stateless_rpc = 1;
 				continue;
diff --git a/remote.h b/remote.h
index 8b62efd..f346524 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 1ccc84c..08602a8 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -190,7 +190,7 @@ 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)
+static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args, int *atomic_push_failed)
 {
 	if (!ref->peer_ref && !args->send_mirror)
 		return 0;
@@ -203,6 +203,12 @@ 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:
+		if (atomic_push_failed) {
+			fprintf(stderr, "Atomic push failed for ref %s. "
+				"Status:%d\n", ref->name, ref->status);
+			*atomic_push_failed = 1;
+		}
+		/* fallthrough */
 	case REF_STATUS_UPTODATE:
 		return 0;
 	default:
@@ -250,7 +256,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 (!ref_update_to_be_sent(ref, args, NULL))
 			continue;
 		update_seen = 1;
 		strbuf_addf(&cert, "%s %s %s\n",
@@ -297,7 +303,7 @@ int send_pack(struct send_pack_args *args,
 	int atomic_push_supported = 0;
 	int atomic_push = 0;
 	unsigned cmds_sent = 0;
-	int ret;
+	int ret, atomic_push_failed = 0;
 	struct async demux;
 	const char *push_cert_nonce = NULL;
 
@@ -332,6 +338,11 @@ int send_pack(struct send_pack_args *args,
 			"Perhaps you should specify a branch such as 'master'.\n");
 		return 0;
 	}
+	if (args->use_atomic_push && !atomic_push_supported) {
+		fprintf(stderr, "Server does not support atomic-push.");
+		return -1;
+	}
+	atomic_push = atomic_push_supported && args->use_atomic_push;
 
 	if (status_report)
 		strbuf_addstr(&cap_buf, " report-status");
@@ -365,7 +376,8 @@ 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))
+		if (!ref_update_to_be_sent(ref, args,
+			args->use_atomic_push ? &atomic_push_failed : NULL))
 			continue;
 
 		if (!ref->deletion)
@@ -377,6 +389,23 @@ int send_pack(struct send_pack_args *args,
 			ref->status = REF_STATUS_EXPECTING_REPORT;
 	}
 
+	if (atomic_push_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:
+				; /* do nothing */
+			}
+		}
+		fprintf(stderr, "Atomic push failed.");
+		return -1;
+	}
+
 	/*
 	 * Finally, tell the other end!
 	 */
@@ -386,7 +415,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 (!ref_update_to_be_sent(ref, args, NULL))
 			continue;
 
 		old_hex = sha1_to_hex(ref->old_sha1);
diff --git a/send-pack.h b/send-pack.h
index 5635457..7486e65 100644
--- a/send-pack.h
+++ b/send-pack.h
@@ -11,6 +11,7 @@ struct send_pack_args {
 		force_update:1,
 		use_thin_pack:1,
 		use_ofs_delta:1,
+		use_atomic_push:1,
 		dry_run:1,
 		push_cert:1,
 		stateless_rpc:1;
diff --git a/transport.c b/transport.c
index 70d38e4..e87481d 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;
-- 
2.2.0.33.gc2219e3.dirty
Junio C Hamano· Dec 15, 2014, 21:01 UTC · re: Stefan Beller · lore

Re: [PATCH 2/5] send-pack.c: add an --atomic-push command line argument

Stefan Beller <sbeller@google.com> writes:
> -static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args)
> +static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args, int *atomic_push_failed)

Hmph. Is "atomic push" so special that it deserves a separate parameter? When we come up with yet another mode of failure, would we add another parameter to the callers to this function?

Show 8 quoted lines
> @@ -203,6 +203,12 @@ 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:
> +		if (atomic_push_failed) {
> +			fprintf(stderr, "Atomic push failed for ref %s. "
> +				"Status:%d\n", ref->name, ref->status);
> +			*atomic_push_failed = 1;

All other error message that come from the codepaths around here seem to avoid uppercase. Also maybe you want to use error() here?

> +	if (args->use_atomic_push && !atomic_push_supported) {
> +		fprintf(stderr, "Server does not support atomic-push.");
> +		return -1;
> +	}
This check logically belongs to the previous step, no?
> +	atomic_push = atomic_push_supported && args->use_atomic_push;
Show 32 quoted lines
>  
>  	if (status_report)
>  		strbuf_addstr(&cap_buf, " report-status");
> @@ -365,7 +376,8 @@ 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))
> +		if (!ref_update_to_be_sent(ref, args,
> +			args->use_atomic_push ? &atomic_push_failed : NULL))
>  			continue;
>  
>  		if (!ref->deletion)
> @@ -377,6 +389,23 @@ int send_pack(struct send_pack_args *args,
>  			ref->status = REF_STATUS_EXPECTING_REPORT;
>  	}
>  
> +	if (atomic_push_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:
> +				; /* do nothing */
> +			}
> +		}
> +		fprintf(stderr, "Atomic push failed.");
> +		return -1;
The same comment as the other fprintf() applies here.
Show 7 quoted lines
> @@ -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);
Why dashed-words-here?
> +		break;
>  	case REF_STATUS_OK:
>  		print_ok_ref_status(ref, porcelain);
>  		break;
Stefan Beller· Dec 15, 2014, 19:56 UTC · re: Stefan Beller · lore

[PATCH 3/5] receive-pack.c: use a single ref_transaction for atomic pushes

From: Ronnie Sahlberg <sahlberg@google.com>

Update receive-pack to use an atomic transaction iff the client negotiated that it wanted atomic-push. This leaves the default behavior to be the old non-atomic one ref at a time update. This is to cause as little disruption as possible to existing clients. It is unknown if there are client scripts that depend on the old non-atomic behavior so we make it opt-in for now.

If it turns out over time that there are no client scripts that depend on the old behavior we can change git to default to use atomic pushes and instead offer an opt-out argument for people that do not want atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
 builtin/receive-pack.c | 73 +++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 58 insertions(+), 15 deletions(-)
Show changes to builtin/receive-pack.c +58 −15
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 0c642ab..e6cf174 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -67,6 +67,8 @@ static const char *NONCE_SLOP = "SLOP";
 static const char *nonce_status;
 static long nonce_stamp_slop;
 static unsigned long nonce_stamp_slop_limit;
+struct strbuf err = STRBUF_INIT;
+struct ref_transaction *transaction;
 
 static enum deny_action parse_deny_action(const char *var, const char *value)
 {
@@ -832,34 +834,56 @@ 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);
-			return "failed to delete";
+		if (!use_atomic_push) {
+			if (delete_ref(namespaced_name, old_sha1, 0)) {
+				rp_error("failed to delete %s", name);
+				return "failed to delete";
+			}
+		} else {
+			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";
+			}
 		}
 		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 (!use_atomic_push) {
+			transaction = ref_transaction_begin(&err);
+			if (!transaction) {
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				return "failed to start 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);
+		if (!use_atomic_push) {
+			if (ref_transaction_commit(transaction, &err)) {
+				ref_transaction_free(transaction);
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				return "failed to update ref";
+			}
+			ref_transaction_free(transaction);
+		}
 		strbuf_release(&err);
 		return NULL; /* good */
 	}
@@ -1059,6 +1083,16 @@ static void execute_commands(struct command *commands,
 		return;
 	}
 
+	if (use_atomic_push) {
+		transaction = ref_transaction_begin(&err);
+		if (!transaction) {
+			error("%s", err.buf);
+			strbuf_release(&err);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = "transaction error";
+			return;
+		}
+	}
 	data.cmds = commands;
 	data.si = si;
 	if (check_everything_connected(iterate_receive_command_list, 0, &data))
@@ -1096,6 +1130,14 @@ static void execute_commands(struct command *commands,
 		}
 	}
 
+	if (use_atomic_push) {
+		if (ref_transaction_commit(transaction, &err)) {
+			rp_error("%s", err.buf);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = err.buf;
+		}
+		ref_transaction_free(transaction);
+	}
 	if (shallow_update && !checked_connectivity)
 		error("BUG: run 'git fsck' for safety.\n"
 		      "If there are errors, try to remove "
@@ -1543,5 +1585,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	sha1_array_clear(&shallow);
 	sha1_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	strbuf_release(&err);
 	return 0;
 }
-- 
2.2.0.33.gc2219e3.dirty
Junio C Hamano· Dec 15, 2014, 21:37 UTC · re: Stefan Beller · lore

Re: [PATCH 3/5] receive-pack.c: use a single ref_transaction for atomic pushes

Stefan Beller <sbeller@google.com> writes:
Show 22 quoted lines
> @@ -832,34 +834,56 @@ 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);
> -			return "failed to delete";
> +		if (!use_atomic_push) {
> +			if (delete_ref(namespaced_name, old_sha1, 0)) {
> +				rp_error("failed to delete %s", name);
> +				return "failed to delete";
> +			}
> +		} else {
> +			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";
> +			}

Doesn't the asymmetry between the above (if transaction is there, use it, otherwise call delete_ref() which conceptually has its sole operation inside a single transaction by itself) and below (if transaction is not there, create it and do its thing, and close the transaction if we created it) bother you?

The above look much simpler, and if it does not switch on use_atomic_push but on the presense of transaction, it would have been even better, i.e.

	if (transaction
            ? ref_transaction_delete(transaction, ...)
            : delete_ref(...)) {
		error(...);
                return "failed to delete";
	}

I think it makes the code harder to read and maintain if you forced a caller of the ref API that happen to touch only a single ref to make three calls to

	ref_transaction_begin();
        ref_transaction_do_one_thing();
        ref_transaction_commit();
instead of making a single call to a simple wrapper
	ref_do_one_thing();

I think that I saw Michael make a similar observation in a near-by thread.

Even if you insist using transactions explicitly in the user of the ref API, I think a better code organization is possible in this particular codepath. Because execute_commands() has the loop over all the proposed updates, why should the update() even need to know how to open a new transaction and when? In other words, can't the code be more like this?

	static update(transaction, ...)
        {
		/* do my thing in the transaction given to me */
                compute what kind of update is needed;
                switch (kind) {
		case delete:
			ref_transaction_delete(transaction, ...);
			break;
		case update:
			ref_transaction_update(transaction, ...);
			break;
		...
		}
	}
	execute_commands(...)
        {
		if (atomic)
                	transaction = ref_transaction_begin(...);
        	for (cmd = commands; cmd; cmd = cmd->next) {
			if (!atomic)        	
                        	transaction = ref_transaction_begin(...);
			update(transaction, ...);
			if (!atomic)
                        	ref_transaction_commit(transaction);
		}
		if (atomic)
                       	ref_transaction_commit(transaction);
	}

That is, update() assumes it is always in _some_ transaction, and execute_commands(), which is what drives multi-ref updates, knows if it wants its repeated calls to update() to be in a single transaction or separate transactions.

Stefan Beller· Dec 15, 2014, 19:56 UTC · re: Stefan Beller · lore

[PATCH 4/5] push.c: add an --atomic-push argument

From: Ronnie Sahlberg <sahlberg@google.com>

Add a command line argument to the git push command to request atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
 Documentation/git-push.txt | 8 +++++++-
 builtin/push.c             | 2 ++
 transport.c                | 1 +
 transport.h                | 1 +
 4 files changed, 11 insertions(+), 1 deletion(-)
Show changes to 4 files +11 −1

Documentation/git-push.txt, builtin/push.c, transport.c, transport.h

diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
index 21b3f29..3e0cb6e 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-push] [-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,12 @@ already exists on the remote side.
 	logged.  See linkgit:git-receive-pack[1] for the details
 	on the receiving end.
 
+--atomic-push::
+	Using atomic push. If atomic push is negotiated with the server
+	then any push covering multiple refs will be atomic. 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/builtin/push.c b/builtin/push.c
index a076b19..ae393c5 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -518,6 +518,8 @@ 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_BIT(0, "atomic-push", &flags, N_("use atomic push, if available"),
+			TRANSPORT_ATOMIC_PUSH),
 		OPT_END()
 	};
 
diff --git a/transport.c b/transport.c
index e87481d..ba1499e 100644
--- a/transport.c
+++ b/transport.c
@@ -830,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.use_atomic_push = !!(flags & TRANSPORT_ATOMIC_PUSH);
 	args.url = transport->url;
 
 	ret = send_pack(&args, data->fd, data->conn, remote_refs,
diff --git a/transport.h b/transport.h
index 3e0091e..25fa1da 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_ATOMIC_PUSH 4096
 
 #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
 #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)
-- 
2.2.0.33.gc2219e3.dirty
Junio C Hamano· Dec 15, 2014, 21:50 UTC · re: Stefan Beller · lore

Re: [PATCH 4/5] push.c: add an --atomic-push argument

Stefan Beller <sbeller@google.com> writes:
Show 8 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> Add a command line argument to the git push command to request atomic
> pushes.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
Makes sense, modulo a nit.
Show 5 quoted lines
> +--atomic-push::
> +	Using atomic push. If atomic push is negotiated with the server
> +	then any push covering multiple refs will be atomic. Either all
> +	refs are updated, or on error, no refs are updated. If the server
> +	does not support atomic pushes the push will fail.
"git push --atomic-push"?  Why should one need to repeat the word?
>  		OPT_BIT(0, "signed", &flags, N_("GPG sign the push"), TRANSPORT_PUSH_CERT),
> +		OPT_BIT(0, "atomic-push", &flags, N_("use atomic push, if available"),
> +			TRANSPORT_ATOMIC_PUSH),
Contrast between the two.  It isn't "git push --signed-push", either ;-)
Stefan Beller· Dec 15, 2014, 19:56 UTC · re: Stefan Beller · lore

[PATCH 5/5] t5543-atomic-push.sh: add basic tests for atomic pushes

This adds tests for the atomic push option. The first four tests check if the atomic option works in good conditions and the last three patches check if the atomic option prevents any change to be pushed if just one ref cannot be updated.

Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Originally Ronnie had a similar patch prepared. But as I added
    more tests and cleaned up the existing tests (e.g. use test_commit
    instead of "echo one >file && gitadd file && git commit -a -m 'one'",
    removal of dead code), the file has changed so much that I'd rather
    take ownership.
 t/t5543-atomic-push.sh | 185 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 185 insertions(+)
 create mode 100755 t/t5543-atomic-push.sh
Show changes to t/t5543-atomic-push.sh +185 −0
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
new file mode 100755
index 0000000..6cbedc5
--- /dev/null
+++ b/t/t5543-atomic-push.sh
@@ -0,0 +1,185 @@
+#!/bin/sh
+
+test_description='pushing to a repository using the atomic push option'
+
+. ./test-lib.sh
+
+D=`pwd`
+
+mk_repo_pair () {
+	rm -rf workbench upstream thirdparty &&
+	mkdir upstream &&
+	(
+		cd upstream &&
+		git init --bare #&&
+		#git config receive.denyCurrentBranch warn
+	) &&
+	mkdir workbench &&
+	(
+		cd workbench &&
+		git init &&
+		git remote add up ../upstream
+	)
+}
+
+test_push_failed () {
+	workbench_master=$(cd workbench && git show-ref -s --verify refs/heads/master) &&
+	upstream_master=$(cd upstream && git show-ref -s --verify refs/heads/master) &&
+	test "$workbench_master" != "$upstream_master" &&
+
+	workbench_second=$(cd workbench && git show-ref -s --verify refs/heads/second) &&
+	upstream_second=$(cd upstream && git show-ref -s --verify refs/heads/second) &&
+	test "$workbench_second" != "$upstream_second"
+}
+
+test_push_succeeded () {
+	workbench_master=$(cd workbench && git show-ref -s --verify refs/heads/master) &&
+	upstream_master=$(cd upstream && git show-ref -s --verify refs/heads/master) &&
+	test "$workbench_master" = "$upstream_master"
+
+	workbench_second=$(cd workbench && git show-ref -s --verify refs/heads/second) &&
+	upstream_second=$(cd upstream && git show-ref -s --verify refs/heads/second) &&
+	test "$workbench_second" = "$upstream_second"
+}
+
+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-push --mirror up
+	) &&
+	workbench_master=$(cd workbench && git show-ref -s --verify refs/heads/master) &&
+	upstream_master=$(cd upstream && git show-ref -s --verify refs/heads/master) &&
+	test "$workbench_master" = "$upstream_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-push --mirror up
+	) &&
+	test_push_succeeded
+'
+
+test_expect_success 'atomic push works in combination with --mirror' '
+	mk_repo_pair &&
+	ls workbench &&
+	(
+		cd workbench &&
+		test_commit one &&
+		git checkout -b second &&
+		test_commit two &&
+		git push --atomic-push --mirror up
+	) &&
+	test_push_succeeded
+'
+
+test_expect_success 'atomic push works in combination with --force' '
+	mk_repo_pair &&
+	(
+		cd workbench &&
+		test_commit one &&
+		git checkout -b second &&
+		test_commit two &&
+		test_commit three &&
+		test_commit four &&
+		git push --mirror up &&
+		git reset --hard HEAD^ &&
+		git push --force --atomic-push up master second
+	) &&
+	test_push_succeeded
+'
+
+# 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 locally' '
+	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 &&
+		git checkout master &&
+		test_commit five &&
+		! git push --atomic-push --all up
+	) &&
+	test_push_failed
+'
+
+test_expect_success 'atomic push fails if one branch 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:
+	(
+		git clone upstream thirdparty &&
+		cd thirdparty
+		git checkout second
+		test_commit unrelated_changes &&
+		git push origin second
+	) &&
+	# see if we can now push both branches.
+	(
+		cd workbench &&
+		git checkout master &&
+		test_commit three &&
+		git checkout second &&
+		test_commit four &&
+		! git push --atomic-push up master second
+	) &&
+	test_push_failed
+'
+
+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:
+	(
+		git clone upstream thirdparty &&
+		cd thirdparty
+		git checkout second
+		git tag test_tag &&
+		git push --tags origin 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 &&
+		! git push --tags --atomic-push up master second
+	) &&
+	test_push_failed
+'
+
+test_done
-- 
2.2.0.33.gc2219e3.dirty
Junio C Hamano· Dec 15, 2014, 22:29 UTC · re: Stefan Beller · lore

Re: [PATCH 5/5] t5543-atomic-push.sh: add basic tests for atomic pushes

Stefan Beller <sbeller@google.com> writes:
Show 33 quoted lines
> This adds tests for the atomic push option.
> The first four tests check if the atomic option works in
> good conditions and the last three patches check if the atomic
> option prevents any change to be pushed if just one ref cannot
> be updated.
>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>
> Notes:
>     Originally Ronnie had a similar patch prepared. But as I added
>     more tests and cleaned up the existing tests (e.g. use test_commit
>     instead of "echo one >file && gitadd file && git commit -a -m 'one'",
>     removal of dead code), the file has changed so much that I'd rather
>     take ownership.
>
>  t/t5543-atomic-push.sh | 185 +++++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 185 insertions(+)
>  create mode 100755 t/t5543-atomic-push.sh
>
> diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
> new file mode 100755
> index 0000000..6cbedc5
> --- /dev/null
> +++ b/t/t5543-atomic-push.sh
> @@ -0,0 +1,185 @@
> +#!/bin/sh
> +
> +test_description='pushing to a repository using the atomic push option'
> +
> +. ./test-lib.sh
> +
> +D=`pwd`
$(pwd)???
Show 7 quoted lines
> +mk_repo_pair () {
> +	rm -rf workbench upstream thirdparty &&
> +	mkdir upstream &&
> +	(
> +		cd upstream &&
> +		git init --bare #&&
> +		#git config receive.denyCurrentBranch warn
Please drop unused comments; they are distracting.
Show 8 quoted lines
> +	) &&
> +	mkdir workbench &&
> +	(
> +		cd workbench &&
> +		git init &&
> +		git remote add up ../upstream
> +	)
> +}

Hmph. Shouldn't you be using test_create_repo to create these, so that templates are used from the build (not install) location, and their hooks are disabled?

> +test_push_failed () {
Does that mean "test_push_must_fail"?
Show 7 quoted lines
> +	workbench_master=$(cd workbench && git show-ref -s --verify refs/heads/master) &&
> +	upstream_master=$(cd upstream && git show-ref -s --verify refs/heads/master) &&
> +	test "$workbench_master" != "$upstream_master" &&
> +
> +	workbench_second=$(cd workbench && git show-ref -s --verify refs/heads/second) &&
> +	upstream_second=$(cd upstream && git show-ref -s --verify refs/heads/second) &&
> +	test "$workbench_second" != "$upstream_second"

So the tests that use this helper always try to update master and second? Is "they are different" the only thing that matters? Or should you be verifying "They are left as the same value they used to have before the attempted push that must have failed"?

It might be a good time to use "-C" option, e.g. "git -C workbench show-ref ...", a bit more in our tests.

Show 6 quoted lines
> +}
> +
> +test_push_succeeded () {
> +	workbench_master=$(cd workbench && git show-ref -s --verify refs/heads/master) &&
> +	upstream_master=$(cd upstream && git show-ref -s --verify refs/heads/master) &&
> +	test "$workbench_master" = "$upstream_master"
Broken &&-chain?
Show 18 quoted lines
> +
> +	workbench_second=$(cd workbench && git show-ref -s --verify refs/heads/second) &&
> +	upstream_second=$(cd upstream && git show-ref -s --verify refs/heads/second) &&
> +	test "$workbench_second" = "$upstream_second"
> +}
> +
> +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-push --mirror up
> +	) &&
> +	workbench_master=$(cd workbench && git show-ref -s --verify refs/heads/master) &&
> +	upstream_master=$(cd upstream && git show-ref -s --verify refs/heads/master) &&
> +	test "$workbench_master" = "$upstream_master"

Hmph. It is a shame that the nice helper you created cannot be used here.

Show 16 quoted lines
> +'
> +
> +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-push --mirror up
> +	) &&
> +	test_push_succeeded
> +'
OK.
> +test_expect_success 'atomic push works in combination with --mirror' '
> +	mk_repo_pair &&
> +	ls workbench &&
Debugging?
Show 6 quoted lines
> +	(
> +		cd workbench &&
> +		test_commit one &&
> +		git checkout -b second &&
> +		test_commit two &&
> +		git push --atomic-push --mirror up

It is not wrong per-se, but haven't you already tested in combination with --mirror in the previous test?

Show 19 quoted lines
> +	) &&
> +	test_push_succeeded
> +'
> +
> +test_expect_success 'atomic push works in combination with --force' '
> +	mk_repo_pair &&
> +	(
> +		cd workbench &&
> +		test_commit one &&
> +		git checkout -b second &&
> +		test_commit two &&
> +		test_commit three &&
> +		test_commit four &&
> +		git push --mirror up &&
> +		git reset --hard HEAD^ &&
> +		git push --force --atomic-push up master second
> +	) &&
> +	test_push_succeeded
> +'
OK.
Show 13 quoted lines
> +# 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 locally' '
> +	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
Broken &&-chain.
> +		git reset --hard HEAD~2 &&
> +		git checkout master &&
> +		test_commit five &&
> +		! git push --atomic-push --all up
test_must_fail?
> +	) &&
> +	test_push_failed

You not only want to see master and second in upstream different from those in workbench, but they point at specific commits that the previous mirror push updated to.

Instead of test_push_failed / test_push_succeeded, how about a single helper that takes the two commit object names you expect these two branches point at? E.g.

	check_branches upstream master HEAD@{2} second HEAD~

(I am probably miscounting HEAD@{$offset} here; this is for illustration only) that roughly does

	check_branches () {
		target=$1
                shift
                while test $# -ne 0
                do
			git -C "$target" rev-parse --verify "refs/heads/$1" >actual &&
			git rev-parse "$2" >expect &&
			test_cmp expect actual || return 1
			shift
                        shift
		done
	}
or something like that?
Show 29 quoted lines
> +test_expect_success 'atomic push fails if one branch 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:
> +	(
> +		git clone upstream thirdparty &&
> +		cd thirdparty
> +		git checkout second
> +		test_commit unrelated_changes &&
> +		git push origin second
> +	) &&
> +	# see if we can now push both branches.
> +	(
> +		cd workbench &&
> +		git checkout master &&
> +		test_commit three &&
> +		git checkout second &&
> +		test_commit four &&
> +		! git push --atomic-push up master second
> +	) &&
> +	test_push_failed
> +'

What's the value of this test? Isn't it a non-fast-forward check you already tested in the previous one?

Show 18 quoted lines
> +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:
> +	(
> +		git clone upstream thirdparty &&
> +		cd thirdparty
> +		git checkout second
> +		git tag test_tag &&
> +		git push --tags origin second
> +	) &&

Broken &&-chain aside, you do not need thirdparty. Doing the "git tag" inside upstream itself should suffice.

Show 9 quoted lines
> +	# 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 &&
> +		! git push --tags --atomic-push up master second
test_must_fail?
> +	) &&
> +	test_push_failed
> +'

One more failure mode you would want to check is what if "update" hook in the receiving repository rejected one update (but not the other). Something along the lines of:

	... setup ...
	git -C workbench push up &&
        write_script upstream/hooks/update <<-\EOF &&
	# only allow update to master from now on
        test "$1" = "refs/heads/master"
        EOF
        ... further update to master and second ...
        test_must_fail git -C workbench push up
	check_branches upstream master ... second ...
perhaps?
Junio C Hamano· Dec 15, 2014, 22:33 UTC · re: Stefan Beller · lore

Re: [PATCH 0/5] Add a flag to push atomically

Stefan Beller <sbeller@google.com> writes:
Show 24 quoted lines
> This patch series adds a flag to git push to update the remote refs atomically.
>
> It was part of a longer patch series[1].
> This series applies on top of origin/mh/reflog-expire
> It can also be found at [2].
>
> Change since picking the series up from Ronnie.
>   * other anchor point (i.e. where the series applies)
>   * more tests for this feature, specially testing failures
>   * drop the patch to rename ref_transaction_* to transaction_*_ref
>   * slight rewording of the additional documentation
>
> [1] http://www.spinics.net/lists/git/msg241214.html
> [2] https://github.com/stefanbeller/git/tree/atomic-push-v1
>
> Ronnie Sahlberg (4):
>   receive-pack.c: add protocol support to negotiate atomic-push
>   send-pack.c: add an --atomic-push command line argument
>   receive-pack.c: use a single transaction when atomic-push is
>     negotiated
>   push.c: add an --atomic-push argument
>
> Stefan Beller (1):
>   t5543-atomic-push.sh: add basic tests for atomic pushes
Thanks.  I think I like where it is going.
Stefan Beller· Dec 16, 2014, 18:49 UTC · re: Junio C Hamano · lore

[PATCHv2 1/6] receive-pack.c: add protocol support to negotiate atomic-push

From: Ronnie Sahlberg <sahlberg@google.com>
This adds support to the protocol between send-pack and receive-pack to
* allow receive-pack to inform the client that it has atomic push capability
* allow send-pack to request atomic push back.

There is currently no setting in send-pack to actually request that atomic pushes are to be used yet. This only adds protocol capability not ability for the user to activate it.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2:
    	* Name it atomic instead of atomic-push. The name changes affects
    	  the flags send over the wire as well as the flags in
    	  struct send_pack_args
    	* Add check, which was part of the later patch here:
    	if (args->atomic && !atomic_supported) {
    		fprintf(stderr, "Server does not support atomic push.");
    		return -1;
    	}
 Documentation/technical/protocol-capabilities.txt | 13 +++++++++++--
 builtin/receive-pack.c                            |  6 +++++-
 send-pack.c                                       | 11 +++++++++++
 send-pack.h                                       |  3 ++-
 4 files changed, 29 insertions(+), 4 deletions(-)
Show changes to 4 files +29 −4

Documentation/technical/protocol-capabilities.txt, builtin/receive-pack.c, send-pack.c, send-pack.h

diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 6d5424c..68ec23d 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 single atomic transaction. Either all refs are
+updated or none.
+
 allow-tip-sha1-in-want
 ----------------------
 
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 32fc540..e76e5d5 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -40,6 +40,7 @@ static int transfer_unpack_limit = -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;
@@ -171,7 +172,8 @@ static void show_ref(const char *path, const unsigned char *sha1)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status delete-refs side-band-64k quiet "
+			      "atomic");
 		if (prefer_ofs_delta)
 			strbuf_addstr(&cap, " ofs-delta");
 		if (push_cert_nonce)
@@ -1179,6 +1181,8 @@ 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 (parse_feature_request(feature_list, "atomic"))
+				use_atomic = 1;
 		}
 
 		if (!strcmp(line, "push-cert")) {
diff --git a/send-pack.c b/send-pack.c
index 949cb61..2a513f4 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -294,6 +294,8 @@ int send_pack(struct send_pack_args *args,
 	int use_sideband = 0;
 	int quiet_supported = 0;
 	int agent_supported = 0;
+	int use_atomic;
+	int atomic_supported = 0;
 	unsigned cmds_sent = 0;
 	int ret;
 	struct async demux;
@@ -314,6 +316,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;
 
@@ -328,6 +332,11 @@ 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) {
+		fprintf(stderr, "Server does not support atomic push.");
+		return -1;
+	}
+	use_atomic = atomic_supported && args->atomic;
 
 	if (status_report)
 		strbuf_addstr(&cap_buf, " report-status");
@@ -335,6 +344,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());
 
diff --git a/send-pack.h b/send-pack.h
index 5635457..b664648 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,
-- 
2.2.0.31.gad78000.dirty
Stefan Beller· Dec 16, 2014, 18:49 UTC · re: Stefan Beller · lore

[PATCHv2 2/6] send-pack: Invert the return value of ref_update_to_be_sent

Having the return value inverted we can have different values for the error codes. This is useful in a later patch when we want to know if we hit the REF_STATUS_REJECT_* case.

Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    New in the series. For consistency with all the other patches
    it also reads v2.
 send-pack.c | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)
Show changes to send-pack.c +8 −7
diff --git a/send-pack.c b/send-pack.c
index 2a513f4..1c4ac75 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -190,10 +190,10 @@ 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)
+static int no_ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args)
 {
 	if (!ref->peer_ref && !args->send_mirror)
-		return 0;
+		return 1;
 
 	/* Check for statuses set by set_ref_status_for_push() */
 	switch (ref->status) {
@@ -203,10 +203,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 2;
 	case REF_STATUS_UPTODATE:
-		return 0;
+		return 3;
 	default:
-		return 1;
+		return 0;
 	}
 }
 
@@ -250,7 +251,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 (no_ref_update_to_be_sent(ref, args))
 			continue;
 		update_seen = 1;
 		strbuf_addf(&cert, "%s %s %s\n",
@@ -370,7 +371,7 @@ 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))
+		if (no_ref_update_to_be_sent(ref, args))
 			continue;
 
 		if (!ref->deletion)
@@ -391,7 +392,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 (no_ref_update_to_be_sent(ref, args))
 			continue;
 
 		old_hex = sha1_to_hex(ref->old_sha1);
-- 
2.2.0.31.gad78000.dirty
Junio C Hamano· Dec 16, 2014, 19:14 UTC · re: Stefan Beller · lore

Re: [PATCHv2 2/6] send-pack: Invert the return value of ref_update_to_be_sent

Stefan Beller <sbeller@google.com> writes:
Show 10 quoted lines
> Having the return value inverted we can have different values for
> the error codes. This is useful in a later patch when we want to
> know if we hit the REF_STATUS_REJECT_* case.
>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>
> Notes:
>     New in the series. For consistency with all the other patches
>     it also reads v2.
Sorry, but ECANNOTPARSE especially "it also read v2" part.
Show 31 quoted lines
>  send-pack.c | 15 ++++++++-------
>  1 file changed, 8 insertions(+), 7 deletions(-)
>
> diff --git a/send-pack.c b/send-pack.c
> index 2a513f4..1c4ac75 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -190,10 +190,10 @@ 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)
> +static int no_ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args)
>  {
>  	if (!ref->peer_ref && !args->send_mirror)
> -		return 0;
> +		return 1;
>  
>  	/* Check for statuses set by set_ref_status_for_push() */
>  	switch (ref->status) {
> @@ -203,10 +203,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 2;
>  	case REF_STATUS_UPTODATE:
> -		return 0;
> +		return 3;
>  	default:
> -		return 1;
> +		return 0;

Hmmm. It used to be "will we send this one?" It is fine if the function wants to differenciate various kinds of error, but

 (1) unexplained 1, 2 and 3 looks cryptic and unmaintainable;
 (2) no_ prefix in the function name is usually a bad idea, because
     it easily invites "if (!no_foo(...))" double negation; and
 (3) unless there is a good reason to do otherwise, a function that
     returns 0 on success should signal an error with a negative
     return.
"static int check_to_send_update()" or something, perhaps?
	if (check_to_send_update() < 0)
        	/* skip as this is an error */
                continue;
does not look too bad.
Show 29 quoted lines
>  }
>  
> @@ -250,7 +251,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 (no_ref_update_to_be_sent(ref, args))
>  			continue;
>  		update_seen = 1;
>  		strbuf_addf(&cert, "%s %s %s\n",
> @@ -370,7 +371,7 @@ 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))
> +		if (no_ref_update_to_be_sent(ref, args))
>  			continue;
>  
>  		if (!ref->deletion)
> @@ -391,7 +392,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 (no_ref_update_to_be_sent(ref, args))
>  			continue;
>  
>  		old_hex = sha1_to_hex(ref->old_sha1);
Stefan Beller· Dec 16, 2014, 18:49 UTC · re: Stefan Beller · lore

[PATCHv2 3/6] send-pack.c: add --atomic command line argument

From: Ronnie Sahlberg <sahlberg@google.com>

This adds support to send-pack to negotiate and use atomic pushes iff the server supports it. Atomic pushes are activated by a new command line flag --atomic.

In order to do this we also need to change the semantics for send_pack() slightly. The existing send_pack() function actually doesn't send all the refs back to the server when multiple refs are involved, for example when using --all. Several of the failure modes for pushes can already be detected locally in the send_pack client based on the information from the initial server side list of all the refs as generated by receive-pack. Any such refs that we thus know would fail to push are thus pruned from the list of refs we send to the server to update.

For atomic pushes, we have to deal thus with both failures that are detected locally as well as failures that are reported back from the server. In order to do so we treat all local failures as push failures too.

We introduce a new status code REF_STATUS_ATOMIC_PUSH_FAILED so we can flag all refs that we would normally have tried to push to the server but we did not due to local failures. This is to improve the error message back to the end user to flag that "these refs failed to update since the atomic push operation failed."

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2:
     * Now we only need a very small change in the existing code and have
       a new static function, which cares about error reporting.
    	  Junio wrote:
    	  > Hmph.  Is "atomic push" so special that it deserves a separate
    	  > parameter?  When we come up with yet another mode of failure, would
    	  > we add another parameter to the callers to this function?
     * error messages are worded differently (lower case!),
     * use of error function instead of fprintf
    
     * undashed the printed error message ("atomic push failed");
 Documentation/git-send-pack.txt |  7 ++++++-
 builtin/send-pack.c             |  6 +++++-
 remote.h                        |  3 ++-
 send-pack.c                     | 36 ++++++++++++++++++++++++++++++++++--
 transport.c                     |  4 ++++
 5 files changed, 51 insertions(+), 5 deletions(-)
Show changes to 5 files +51 −5

Documentation/git-send-pack.txt, builtin/send-pack.c, remote.h, send-pack.c, transport.c

diff --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt
index 2a0de42..45c7725 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/builtin/send-pack.c b/builtin/send-pack.c
index b564a77..b961e5a 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 8b62efd..f346524 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 1c4ac75..71b1915 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -279,6 +279,30 @@ 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:
+			; /* do nothing */
+		}
+	}
+	error("atomic push failed for ref %s. "
+	      "status: %d\n", failing_ref->name, failing_ref->status);
+	return -1;
+}
+
 int send_pack(struct send_pack_args *args,
 	      int fd[], struct child_process *conn,
 	      struct ref *remote_refs,
@@ -371,9 +395,17 @@ int send_pack(struct send_pack_args *args,
 	 * the pack data.
 	 */
 	for (ref = remote_refs; ref; ref = ref->next) {
-		if (no_ref_update_to_be_sent(ref, args))
+		int reject_reason;
+		if ((reject_reason = no_ref_update_to_be_sent(ref, args))) {
+			/* 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 && reject_reason == 2)
+				return atomic_push_failure(args,
+							   remote_refs,
+							   ref);
 			continue;
-
+		}
 		if (!ref->deletion)
 			need_pack_data = 1;
 
diff --git a/transport.c b/transport.c
index 70d38e4..c67feee 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;
-- 
2.2.0.31.gad78000.dirty
Junio C Hamano· Dec 16, 2014, 19:31 UTC · re: Stefan Beller · lore

Re: [PATCHv2 3/6] send-pack.c: add --atomic command line argument

Stefan Beller <sbeller@google.com> writes:
Show 6 quoted lines
>      * Now we only need a very small change in the existing code and have
>        a new static function, which cares about error reporting.
>     	  Junio wrote:
>     	  > Hmph.  Is "atomic push" so special that it deserves a separate
>     	  > parameter?  When we come up with yet another mode of failure, would
>     	  > we add another parameter to the callers to this function?
I suspect that you completely mis-read me.

If you add an extra arg that is *specifically* for atomic push, like this:

    -static int update_to_send(...);
    +static int update_to_send(..., int *atomic_push_failed);
    
what signal does it send to the next person who may want to add
a new "nuclear push" option?  Should the patch look like
    -static int update_to_send(..., int *atomic_push_failed);
    +static int update_to_send(..., int *atomic_push_failed, int *nuclear_push_failed);
by adding yet another separate variable for error reporting?

I would have understood if the new variable was named something like "failure_reason", which may be set to PUSH_FAILURE_ATOMIC when an atomic push failure was detected. Making the helper return the reason why the push failed would be another way, like you did in the 2/6 patch in this round.

Either way, the next person would know to add a code to do his "nuclear push" and set the reason to PUSH_FAILURE_NUCLEAR when it fails, instead of piling yet another error reporting variable that is specific to the feature.

This is all about code maintainability, which is very different from "who cares about error reporting".

Show 17 quoted lines
> diff --git a/remote.h b/remote.h
> index 8b62efd..f346524 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;
> ...
>  	for (ref = remote_refs; ref; ref = ref->next) {
> -		if (no_ref_update_to_be_sent(ref, args))
> +		int reject_reason;
> +		if ((reject_reason = no_ref_update_to_be_sent(ref, args))) {
We avoid assignment inside a conditional.
Show 8 quoted lines
> +			/* 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 && reject_reason == 2)
> +				return atomic_push_failure(args,
> +							   remote_refs,
> +							   ref);
>  			continue;
Perhaps
		switch (check_to_send_update(...)) {
                case 0: /* no error */
                	break;
		case -REF_STATUS_ATOMIC_PUSH_FAILED:                        
                	return atomic_push_failure(args, remote_refs, ref);
                	break;
		default:
			continue;
		}
or something?
Stefan Beller· Dec 16, 2014, 18:49 UTC · re: Stefan Beller · lore

[PATCHv2 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

From: Ronnie Sahlberg <sahlberg@google.com>

Update receive-pack to use an atomic transaction iff the client negotiated that it wanted atomic-push. This leaves the default behavior to be the old non-atomic one ref at a time update. This is to cause as little disruption as possible to existing clients. It is unknown if there are client scripts that depend on the old non-atomic behavior so we make it opt-in for now.

If it turns out over time that there are no client scripts that depend on the old behavior we can change git to default to use atomic pushes and instead offer an opt-out argument for people that do not want atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2:
    	* update(...) assumes to be always in a transaction
    	* Caring about when to begin/commit transactions is put
    	  into execute_commands
 builtin/receive-pack.c | 64 ++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 49 insertions(+), 15 deletions(-)
Show changes to builtin/receive-pack.c +49 −15
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e76e5d5..0803fd2 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -67,6 +67,8 @@ static const char *NONCE_SLOP = "SLOP";
 static const char *nonce_status;
 static long nonce_stamp_slop;
 static unsigned long nonce_stamp_slop_limit;
+struct strbuf err = STRBUF_INIT;
+struct ref_transaction *transaction;
 
 static enum deny_action parse_deny_action(const char *var, const char *value)
 {
@@ -832,34 +834,32 @@ 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";
 		}
 		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 */
 	}
@@ -1059,6 +1059,16 @@ static void execute_commands(struct command *commands,
 		return;
 	}
 
+	if (use_atomic) {
+		transaction = ref_transaction_begin(&err);
+		if (!transaction) {
+			error("%s", err.buf);
+			strbuf_release(&err);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = "transaction error";
+			return;
+		}
+	}
 	data.cmds = commands;
 	data.si = si;
 	if (check_everything_connected(iterate_receive_command_list, 0, &data))
@@ -1086,8 +1096,23 @@ static void execute_commands(struct command *commands,
 
 		if (cmd->skip_update)
 			continue;
-
+		if (!use_atomic) {
+			transaction = ref_transaction_begin(&err);
+			if (!transaction) {
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				cmd->error_string = "failed to start transaction";
+			}
+		}
 		cmd->error_string = update(cmd, si);
+		if (!use_atomic)
+			if (ref_transaction_commit(transaction, &err)) {
+				ref_transaction_free(transaction);
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				cmd->error_string = "failed to update ref";
+			}
+
 		if (shallow_update && !cmd->error_string &&
 		    si->shallow_ref[cmd->index]) {
 			error("BUG: connectivity check has not been run on ref %s",
@@ -1096,6 +1121,14 @@ static void execute_commands(struct command *commands,
 		}
 	}
 
+	if (use_atomic) {
+		if (ref_transaction_commit(transaction, &err)) {
+			rp_error("%s", err.buf);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = err.buf;
+		}
+		ref_transaction_free(transaction);
+	}
 	if (shallow_update && !checked_connectivity)
 		error("BUG: run 'git fsck' for safety.\n"
 		      "If there are errors, try to remove "
@@ -1543,5 +1576,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	sha1_array_clear(&shallow);
 	sha1_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	strbuf_release(&err);
 	return 0;
 }
-- 
2.2.0.31.gad78000.dirty
Eric Sunshine· Dec 16, 2014, 19:29 UTC · re: Stefan Beller · lore

Re: [PATCHv2 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

On Tue, Dec 16, 2014 at 1:49 PM, Stefan Beller <sbeller@google.com> wrote:
Show 25 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> Update receive-pack to use an atomic transaction iff the client negotiated
> that it wanted atomic-push. This leaves the default behavior to be the old
> non-atomic one ref at a time update. This is to cause as little disruption
> as possible to existing clients. It is unknown if there are client scripts
> that depend on the old non-atomic behavior so we make it opt-in for now.
>
> If it turns out over time that there are no client scripts that depend on the
> old behavior we can change git to default to use atomic pushes and instead
> offer an opt-out argument for people that do not want atomic pushes.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index e76e5d5..0803fd2 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -67,6 +67,8 @@ static const char *NONCE_SLOP = "SLOP";
>  static const char *nonce_status;
>  static long nonce_stamp_slop;
>  static unsigned long nonce_stamp_slop_limit;
> +struct strbuf err = STRBUF_INIT;
> +struct ref_transaction *transaction;
Should these be static?
Show 16 quoted lines
>  static enum deny_action parse_deny_action(const char *var, const char *value)
>  {
> @@ -1059,6 +1059,16 @@ static void execute_commands(struct command *commands,
>                 return;
>         }
>
> +       if (use_atomic) {
> +               transaction = ref_transaction_begin(&err);
> +               if (!transaction) {
> +                       error("%s", err.buf);
> +                       strbuf_release(&err);
> +                       for (cmd = commands; cmd; cmd = cmd->next)
> +                               cmd->error_string = "transaction error";
> +                       return;
> +               }
> +       }

Not seen in this diff, but just below this point, the pre-receive hook is invoked. If it "declines", then execute_commands() returns without releasing the transaction which was begun here. Is that correct behavior?

For robustness, it might also be sane to release the 'err' strbuf at this early return (though the current code does not strictly leak it).

Show 14 quoted lines
>         data.cmds = commands;
>         data.si = si;
>         if (check_everything_connected(iterate_receive_command_list, 0, &data))
> @@ -1086,8 +1096,23 @@ static void execute_commands(struct command *commands,
>
>                 if (cmd->skip_update)
>                         continue;
> -
> +               if (!use_atomic) {
> +                       transaction = ref_transaction_begin(&err);
> +                       if (!transaction) {
> +                               rp_error("%s", err.buf);
> +                               strbuf_release(&err);
> +                               cmd->error_string = "failed to start transaction";
Here, you assign cmd->error_string...
> +                       }
> +               }
>                 cmd->error_string = update(cmd, si);

and then immediately overwrite it here. Is it correct to invoke update() when ref_transaction_begin() has failed? Seems fishy.

> +               if (!use_atomic)
> +                       if (ref_transaction_commit(transaction, &err)) {
> +                               ref_transaction_free(transaction);

Shouldn't you be freeing the transaction outside of this conditional rather than only in case of failure?

Show 32 quoted lines
> +                               rp_error("%s", err.buf);
> +                               strbuf_release(&err);
> +                               cmd->error_string = "failed to update ref";
> +                       }
> +
>                 if (shallow_update && !cmd->error_string &&
>                     si->shallow_ref[cmd->index]) {
>                         error("BUG: connectivity check has not been run on ref %s",
> @@ -1096,6 +1121,14 @@ static void execute_commands(struct command *commands,
>                 }
>         }
>
> +       if (use_atomic) {
> +               if (ref_transaction_commit(transaction, &err)) {
> +                       rp_error("%s", err.buf);
> +                       for (cmd = commands; cmd; cmd = cmd->next)
> +                               cmd->error_string = err.buf;
> +               }
> +               ref_transaction_free(transaction);
> +       }
>         if (shallow_update && !checked_connectivity)
>                 error("BUG: run 'git fsck' for safety.\n"
>                       "If there are errors, try to remove "
> @@ -1543,5 +1576,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
>         sha1_array_clear(&shallow);
>         sha1_array_clear(&ref);
>         free((void *)push_cert_nonce);
> +       strbuf_release(&err);
>         return 0;
>  }
> --
> 2.2.0.31.gad78000.dirty
Eric Sunshine· Dec 16, 2014, 20:30 UTC · re: Eric Sunshine · lore

Re: [PATCHv2 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

On Tue, Dec 16, 2014 at 2:29 PM, Eric Sunshine <sunshine@sunshineco.com> wrote:
Show 28 quoted lines
> On Tue, Dec 16, 2014 at 1:49 PM, Stefan Beller <sbeller@google.com> wrote:
>> ---
>> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
>> index e76e5d5..0803fd2 100644
>> --- a/builtin/receive-pack.c
>> +++ b/builtin/receive-pack.c
>> @@ -1059,6 +1059,16 @@ static void execute_commands(struct command *commands,
>>                 return;
>>         }
>>
>> +       if (use_atomic) {
>> +               transaction = ref_transaction_begin(&err);
>> +               if (!transaction) {
>> +                       error("%s", err.buf);
>> +                       strbuf_release(&err);
>> +                       for (cmd = commands; cmd; cmd = cmd->next)
>> +                               cmd->error_string = "transaction error";
>> +                       return;
>> +               }
>> +       }
>
> Not seen in this diff, but just below this point, the pre-receive hook
> is invoked. If it "declines", then execute_commands() returns without
> releasing the transaction which was begun here. Is that correct
> behavior?
>
> For robustness, it might also be sane to release the 'err' strbuf at
> this early return (though the current code does not strictly leak it).

To clarify: By "this early return", I mean the early return taken when pre-receive hook declines.

Junio C Hamano· Dec 16, 2014, 19:35 UTC · re: Stefan Beller · lore

Re: [PATCHv2 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

Stefan Beller <sbeller@google.com> writes:
>     	* update(...) assumes to be always in a transaction
>     	* Caring about when to begin/commit transactions is put
>     	  into execute_commands

I am obviously biased, but I find that the new code structure and separation of responsibility between update() and execute() functions a lot clearer than the previous one.

Thanks.
Show 127 quoted lines
>  builtin/receive-pack.c | 64 ++++++++++++++++++++++++++++++++++++++------------
>  1 file changed, 49 insertions(+), 15 deletions(-)
>
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index e76e5d5..0803fd2 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -67,6 +67,8 @@ static const char *NONCE_SLOP = "SLOP";
>  static const char *nonce_status;
>  static long nonce_stamp_slop;
>  static unsigned long nonce_stamp_slop_limit;
> +struct strbuf err = STRBUF_INIT;
> +struct ref_transaction *transaction;
>  
>  static enum deny_action parse_deny_action(const char *var, const char *value)
>  {
> @@ -832,34 +834,32 @@ 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";
>  		}
>  		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 */
>  	}
> @@ -1059,6 +1059,16 @@ static void execute_commands(struct command *commands,
>  		return;
>  	}
>  
> +	if (use_atomic) {
> +		transaction = ref_transaction_begin(&err);
> +		if (!transaction) {
> +			error("%s", err.buf);
> +			strbuf_release(&err);
> +			for (cmd = commands; cmd; cmd = cmd->next)
> +				cmd->error_string = "transaction error";
> +			return;
> +		}
> +	}
>  	data.cmds = commands;
>  	data.si = si;
>  	if (check_everything_connected(iterate_receive_command_list, 0, &data))
> @@ -1086,8 +1096,23 @@ static void execute_commands(struct command *commands,
>  
>  		if (cmd->skip_update)
>  			continue;
> -
> +		if (!use_atomic) {
> +			transaction = ref_transaction_begin(&err);
> +			if (!transaction) {
> +				rp_error("%s", err.buf);
> +				strbuf_release(&err);
> +				cmd->error_string = "failed to start transaction";
> +			}
> +		}
>  		cmd->error_string = update(cmd, si);
> +		if (!use_atomic)
> +			if (ref_transaction_commit(transaction, &err)) {
> +				ref_transaction_free(transaction);
> +				rp_error("%s", err.buf);
> +				strbuf_release(&err);
> +				cmd->error_string = "failed to update ref";
> +			}
> +
>  		if (shallow_update && !cmd->error_string &&
>  		    si->shallow_ref[cmd->index]) {
>  			error("BUG: connectivity check has not been run on ref %s",
> @@ -1096,6 +1121,14 @@ static void execute_commands(struct command *commands,
>  		}
>  	}
>  
> +	if (use_atomic) {
> +		if (ref_transaction_commit(transaction, &err)) {
> +			rp_error("%s", err.buf);
> +			for (cmd = commands; cmd; cmd = cmd->next)
> +				cmd->error_string = err.buf;
> +		}
> +		ref_transaction_free(transaction);
> +	}
>  	if (shallow_update && !checked_connectivity)
>  		error("BUG: run 'git fsck' for safety.\n"
>  		      "If there are errors, try to remove "
> @@ -1543,5 +1576,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
>  	sha1_array_clear(&shallow);
>  	sha1_array_clear(&ref);
>  	free((void *)push_cert_nonce);
> +	strbuf_release(&err);
>  	return 0;
>  }
Stefan Beller· Dec 16, 2014, 18:49 UTC · re: Stefan Beller · lore

[PATCHv2 5/6] push.c: add an --atomic-push argument

From: Ronnie Sahlberg <sahlberg@google.com>

Add a command line argument to the git push command to request atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2
    	It's --atomic now! (dropping the -push)
 Documentation/git-push.txt | 7 ++++++-
 builtin/push.c             | 2 ++
 transport.c                | 1 +
 transport.h                | 1 +
 4 files changed, 10 insertions(+), 1 deletion(-)
Show changes to 4 files +10 −1

Documentation/git-push.txt, builtin/push.c, transport.c, transport.h

diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
index 21b3f29..3feacc5 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-push] [-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.
 
+--atomic::
+	Use the an atomic transaction on the server 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/builtin/push.c b/builtin/push.c
index a076b19..fe0b8cc 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -518,6 +518,8 @@ 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_BIT(0, "atomic", &flags, N_("use a single atomic transaction at the serverside, if available"),
+			TRANSPORT_ATOMIC_PUSH),
 		OPT_END()
 	};
 
diff --git a/transport.c b/transport.c
index c67feee..5b29514 100644
--- a/transport.c
+++ b/transport.c
@@ -830,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_ATOMIC_PUSH);
 	args.url = transport->url;
 
 	ret = send_pack(&args, data->fd, data->conn, remote_refs,
diff --git a/transport.h b/transport.h
index 3e0091e..25fa1da 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_ATOMIC_PUSH 4096
 
 #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
 #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)
-- 
2.2.0.31.gad78000.dirty
Eric Sunshine· Dec 16, 2014, 19:33 UTC · re: Stefan Beller · lore

Re: [PATCHv2 5/6] push.c: add an --atomic-push argument

On Tue, Dec 16, 2014 at 1:49 PM, Stefan Beller <sbeller@google.com> wrote:
Show 23 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> Add a command line argument to the git push command to request atomic
> pushes.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>
> Notes:
>     Changes v1 -> v2
>         It's --atomic now! (dropping the -push)
>
> diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
> index 21b3f29..3feacc5 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-push] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
s/atomic-push/atomic/
Show 9 quoted lines
>            [--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.
>
> +--atomic::
> +       Use the an atomic transaction on the server side if available.
s/the an/an/
Show 15 quoted lines
> +       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/builtin/push.c b/builtin/push.c
> index a076b19..fe0b8cc 100644
> --- a/builtin/push.c
> +++ b/builtin/push.c
> @@ -518,6 +518,8 @@ 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_BIT(0, "atomic", &flags, N_("use a single atomic transaction at the serverside, if available"),
"single atomic" sounds awfully redundant.
"serverside" is odd. Perhaps "server side" or merely "remote" or "remote side".
Show 25 quoted lines
> +                       TRANSPORT_ATOMIC_PUSH),
>                 OPT_END()
>         };
>
> diff --git a/transport.c b/transport.c
> index c67feee..5b29514 100644
> --- a/transport.c
> +++ b/transport.c
> @@ -830,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_ATOMIC_PUSH);
>         args.url = transport->url;
>
>         ret = send_pack(&args, data->fd, data->conn, remote_refs,
> diff --git a/transport.h b/transport.h
> index 3e0091e..25fa1da 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_ATOMIC_PUSH 4096
For consistency with existing names, should this be TRANSPORT_PUSH_ATOMIC?
>  #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
>  #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)
> --
> 2.2.0.31.gad78000.dirty
Junio C Hamano· Dec 16, 2014, 20:43 UTC · re: Eric Sunshine · lore

Re: [PATCHv2 5/6] push.c: add an --atomic-push argument

Eric Sunshine <sunshine@sunshineco.com> writes:
Show 22 quoted lines
>> +--atomic::
>> +       Use the an atomic transaction on the server side if available.
>
> s/the an/an/
> ...
>> +               OPT_BIT(0, "atomic", &flags, N_("use a single atomic transaction at the serverside, if available"),
>
> "single atomic" sounds awfully redundant.
>
> "serverside" is odd. Perhaps "server side" or merely "remote" or "remote side".
> ...
>> diff --git a/transport.h b/transport.h
>> index 3e0091e..25fa1da 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_ATOMIC_PUSH 4096
>
> For consistency with existing names, should this be TRANSPORT_PUSH_ATOMIC?

As always, thanks for a careful reading. I missed everything you pointed out (I agree with you).

Junio C Hamano· Dec 16, 2014, 19:36 UTC · re: Stefan Beller · lore

Re: [PATCHv2 5/6] push.c: add an --atomic-push argument

Stefan Beller <sbeller@google.com> writes:
Show 12 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> Add a command line argument to the git push command to request atomic
> pushes.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>
> Notes:
>     Changes v1 -> v2
>     	It's --atomic now! (dropping the -push)
Also from the subject line ;-)
Show 69 quoted lines
>
>  Documentation/git-push.txt | 7 ++++++-
>  builtin/push.c             | 2 ++
>  transport.c                | 1 +
>  transport.h                | 1 +
>  4 files changed, 10 insertions(+), 1 deletion(-)
>
> diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
> index 21b3f29..3feacc5 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-push] [-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.
>  
> +--atomic::
> +	Use the an atomic transaction on the server 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/builtin/push.c b/builtin/push.c
> index a076b19..fe0b8cc 100644
> --- a/builtin/push.c
> +++ b/builtin/push.c
> @@ -518,6 +518,8 @@ 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_BIT(0, "atomic", &flags, N_("use a single atomic transaction at the serverside, if available"),
> +			TRANSPORT_ATOMIC_PUSH),
>  		OPT_END()
>  	};
>  
> diff --git a/transport.c b/transport.c
> index c67feee..5b29514 100644
> --- a/transport.c
> +++ b/transport.c
> @@ -830,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_ATOMIC_PUSH);
>  	args.url = transport->url;
>  
>  	ret = send_pack(&args, data->fd, data->conn, remote_refs,
> diff --git a/transport.h b/transport.h
> index 3e0091e..25fa1da 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_ATOMIC_PUSH 4096
>  
>  #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
>  #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)
Stefan Beller· Dec 16, 2014, 18:49 UTC · re: Stefan Beller · lore

[PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

This adds tests for the atomic push option. The first four tests check if the atomic option works in good conditions and the last three patches check if the atomic option prevents any change to be pushed if just one ref cannot be updated.

Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2:
    > Please drop unused comments; they are distracting.
    
    ok
    
    > It is not wrong per-se, but haven't you already tested in
    > combination with --mirror in the previous test?
    
    I fixed the previous tests, so that there is no --mirror
    and --atomic together. There is still a first --mirror push
    for setup and a second with --atomic <branchnames> though
    
    > check_branches upstream master HEAD@{2} second HEAD~
    
    A similar function test_ref_upstream is introduced.
    
    > What's the value of this test?  Isn't it a non-fast-forward check
    > you already tested in the previous one?
    
    I messed up there. Originally I wanted to test the 2 different
    stages of rejection. A non-fast-forward check is done locally and
    we don't even try pushing. But I also want to test if we locally
    thing all is good, but the server refuses a ref to update.
    This is now done with the last test named 'atomic push obeys
    update hook preventing a branch to be pushed'. And that still fails.
    
    I'll investigate that, while still sending out the series for another
    review though.
    
    * Redone the test helper, there is test_ref_upstream now.
      This tests explicitely for SHA1 values of the ref.
      (It's needed in the last test for example. The git push fails,
      but still modifies the ref :/ )
    * checked all && chains and repaired them
    * sometimes make use of git -C <workdir>
    
    Notes v1:
    Originally Ronnie had a similar patch prepared. But as I added
    more tests and cleaned up the existing tests (e.g. use test_commit
    instead of "echo one >file && gitadd file && git commit -a -m 'one'",
    removal of dead code), the file has changed so much that I'd rather
    take ownership.
 t/t5543-atomic-push.sh | 176 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 176 insertions(+)
 create mode 100755 t/t5543-atomic-push.sh
Show changes to t/t5543-atomic-push.sh +176 −0
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
new file mode 100755
index 0000000..6354fc0
--- /dev/null
+++ b/t/t5543-atomic-push.sh
@@ -0,0 +1,176 @@
+#!/bin/sh
+
+test_description='pushing to a repository using the atomic push option'
+
+. ./test-lib.sh
+
+D=`pwd`
+
+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
+	)
+}
+
+# refname, expected value, e.g.
+# test_ref_upstream refs/heads/master HEADS{0}
+test_ref_upstream () {
+	test "$#" == "2" # if this fails, we have a bug in this script.
+	test "$(git -C upstream rev-parse --verify $1)" == "$2"
+}
+
+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_ref_upstream master $(git -C workbench rev-parse --verify 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_ref_upstream master $(git -C workbench rev-parse --verify master) &&
+	test_ref_upstream second $(git -C workbench rev-parse --verify 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_ref_upstream master $(git -C workbench rev-parse --verify master) &&
+	test_ref_upstream second $(git -C workbench rev-parse --verify 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_ref_upstream master $(git -C workbench rev-parse --verify master) &&
+	test_ref_upstream second $(git -C workbench rev-parse --verify 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 &&
+		! git push --atomic --all up
+	) &&
+	test_ref_upstream master $(git -C workbench rev-parse --verify HEAD@{7}) &&
+	test_ref_upstream second $(git -C workbench rev-parse --verify 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_ref_upstream master $(git -C workbench rev-parse --verify HEAD@{3}) &&
+	test_ref_upstream second $(git -C workbench rev-parse --verify HEAD@{1})
+'
+
+test_expect_failure '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_ref_upstream master $(git -C workbench rev-parse --verify HEAD@{3}) &&
+	test_ref_upstream second $(git -C workbench rev-parse --verify HEAD@{1})
+'
+
+test_done
-- 
2.2.0.31.gad78000.dirty
Stefan Beller· Dec 16, 2014, 19:14 UTC · re: Stefan Beller · lore

[PATCH] receive-pack: refuse all commands if one fails in atomic mode

This patch will be incorporated into the right places in v3 of the series.
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    I don't want to resend the patch series today to accumulate comments,
    but this makes the last test pass, which is the whole point of the series.
    
    I'll put it into the right places in a reroll.
 builtin/receive-pack.c | 13 ++++++++++++-
 t/t5543-atomic-push.sh |  2 +-
 2 files changed, 13 insertions(+), 2 deletions(-)
Show changes to 2 files +13 −2

builtin/receive-pack.c, t/t5543-atomic-push.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 0803fd2..3477f7c 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1122,7 +1122,18 @@ static void execute_commands(struct command *commands,
 	}
 
 	if (use_atomic) {
-		if (ref_transaction_commit(transaction, &err)) {
+		/* update(...) may abort early (i.e. because the hook refused to
+		 * update that ref), which then doesn't even record a transaction
+		 * regarding that ref. Make sure all commands are without error
+		 * and then commit atomically. */
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (cmd->error_string)
+				break;
+		if (cmd) {
+			for (cmd = commands; cmd; cmd = cmd->next)
+				if (!cmd->error_string)
+					cmd->error_string = "atomic push failure";
+		} else if (ref_transaction_commit(transaction, &err)) {
 			rp_error("%s", err.buf);
 			for (cmd = commands; cmd; cmd = cmd->next)
 				cmd->error_string = err.buf;
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 6354fc0..f0e54d9 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -142,7 +142,7 @@ test_expect_success 'atomic push fails if one tag fails remotely' '
 	test_ref_upstream second $(git -C workbench rev-parse --verify HEAD@{1})
 '
 
-test_expect_failure 'atomic push obeys update hook preventing a branch to be pushed' '
+test_expect_success 'atomic push obeys update hook preventing a branch to be pushed' '
 	mk_repo_pair &&
 	(
 		cd workbench &&
-- 
2.2.0.31.gad78000.dirty
Junio C Hamano· Dec 16, 2014, 20:32 UTC · re: Stefan Beller · lore

Re: [PATCH] receive-pack: refuse all commands if one fails in atomic mode

Stefan Beller <sbeller@google.com> writes:
Show 28 quoted lines
> This patch will be incorporated into the right places in v3 of the series.
>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>
> Notes:
>     I don't want to resend the patch series today to accumulate comments,
>     but this makes the last test pass, which is the whole point of the series.
>     
>     I'll put it into the right places in a reroll.
>
>  builtin/receive-pack.c | 13 ++++++++++++-
>  t/t5543-atomic-push.sh |  2 +-
>  2 files changed, 13 insertions(+), 2 deletions(-)
>
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index 0803fd2..3477f7c 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -1122,7 +1122,18 @@ static void execute_commands(struct command *commands,
>  	}
>  
>  	if (use_atomic) {
> -		if (ref_transaction_commit(transaction, &err)) {
> +		/* update(...) may abort early (i.e. because the hook refused to
> +		 * update that ref), which then doesn't even record a transaction
> +		 * regarding that ref. Make sure all commands are without error
> +		 * and then commit atomically. */
	/*
         * The first line of our multi-line comment
         * has only opening slash-asterisk and nothing else.
         * The last line has asterisk-slash and nothing else.
         */
Show 11 quoted lines
> +		for (cmd = commands; cmd; cmd = cmd->next)
> +			if (cmd->error_string)
> +				break;
> +		if (cmd) {
> +			for (cmd = commands; cmd; cmd = cmd->next)
> +				if (!cmd->error_string)
> +					cmd->error_string = "atomic push failure";
> +		} else if (ref_transaction_commit(transaction, &err)) {
>  			rp_error("%s", err.buf);
>  			for (cmd = commands; cmd; cmd = cmd->next)
>  				cmd->error_string = err.buf;
Makes sense.
Show 13 quoted lines
> diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
> index 6354fc0..f0e54d9 100755
> --- a/t/t5543-atomic-push.sh
> +++ b/t/t5543-atomic-push.sh
> @@ -142,7 +142,7 @@ test_expect_success 'atomic push fails if one tag fails remotely' '
>  	test_ref_upstream second $(git -C workbench rev-parse --verify HEAD@{1})
>  '
>  
> -test_expect_failure 'atomic push obeys update hook preventing a branch to be pushed' '
> +test_expect_success 'atomic push obeys update hook preventing a branch to be pushed' '
>  	mk_repo_pair &&
>  	(
>  		cd workbench &&
Eric Sunshine· Dec 16, 2014, 19:37 UTC · re: Stefan Beller · lore

Re: [PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

On Tue, Dec 16, 2014 at 1:49 PM, Stefan Beller <sbeller@google.com> wrote:
Show 21 quoted lines
> This adds tests for the atomic push option.
> The first four tests check if the atomic option works in
> good conditions and the last three patches check if the atomic
> option prevents any change to be pushed if just one ref cannot
> be updated.
>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
> diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
> new file mode 100755
> index 0000000..6354fc0
> --- /dev/null
> +++ b/t/t5543-atomic-push.sh
> @@ -0,0 +1,176 @@
> +#!/bin/sh
> +
> +test_description='pushing to a repository using the atomic push option'
> +
> +. ./test-lib.sh
> +
> +D=`pwd`

Junio already mentioned this previously, but this should be $(pwd) rather than `pwd`.

However, more importantly, why is this variable declared at all since it is never used by the script?

Show 6 quoted lines
> +mk_repo_pair () {
> +       rm -rf workbench upstream &&
> +       test_create_repo upstream &&
> +       test_create_repo workbench &&
> +       (
> +               cd upstream && git config receive.denyCurrentBranch warn
This would be slightly easier to read if split over two lines.
> +       ) &&
> +       (
> +               cd workbench && git remote add up ../upstream
Ditto.
> +       )
> +}
> +
Junio C Hamano· Dec 16, 2014, 19:46 UTC · re: Stefan Beller · lore

Re: [PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

Stefan Beller <sbeller@google.com> writes:
Show 5 quoted lines
> +test_description='pushing to a repository using the atomic push option'
> +
> +. ./test-lib.sh
> +
> +D=`pwd`
$(pwd)?
Show 7 quoted lines
> +mk_repo_pair () {
> +	rm -rf workbench upstream &&
> +	test_create_repo upstream &&
> +	test_create_repo workbench &&
> +	(
> +		cd upstream && git config receive.denyCurrentBranch warn
> +	) &&

I was wondering how you would do this part after suggesting use of test_create_repo, knowing very well that one of them was a bare one ;-).

We might want to extend test_create_repo to allow creating a bare repository, but this is also OK.

> +	(
> +		cd workbench && git remote add up ../upstream
> +	)
> +}
> +# refname, expected value, e.g.
> +# test_ref_upstream refs/heads/master HEADS{0}
> +test_ref_upstream () {
> +	test "$#" == "2" # if this fails, we have a bug in this script.

This is not C; "test $# = 2" (notice a single equal sign). And you do not need dq-pair around these.

> +	test "$(git -C upstream rev-parse --verify $1)" == "$2"
> +}
Seeing that all callers of test_ref_upstream computes $2 as
	git -C workbench rev-parse --verify <something>
I have a feeling that
> +	test_ref_upstream second second
would be easier for them to write than
> +	test_ref_upstream second $(git -C workbench rev-parse --verify second)
That is
# refname in upstream and expected value from workbench
# E.g. "test_ref_upstream master HEAD" makes sure that HEAD in
# workbench matches the master branch in upstream repository.
test_ref_upstream () {
	test $# = 2 &&
        test "$(git -C upstream rev-parse --verify "$1")" == \
		"$(git -C workbench rev-parse --verify "$2")"
}
or something.  We may however want to do the usual
	test $# = 2 &&
	git -C upstream rev-parse --verify "$1" >expect &&
	git -C workbench rev-parse --verify "$2" >actual &&
        test_cmp expect actual
though.
Stefan Beller· Dec 16, 2014, 19:57 UTC · re: Junio C Hamano · lore

Re: [PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

On Tue, Dec 16, 2014 at 11:46 AM, Junio C Hamano <gitster@pobox.com> wrote:
Show 5 quoted lines
>
> Seeing that all callers of test_ref_upstream computes $2 as
>
>         git -C workbench rev-parse --verify <something>
>

Only in the first tests, where this should be the case after push. In the failure tests, we go with HEAD@{N} which needs to be computed inside the workbench repo.

Alternatively we could check if the reflog of upstream has a certain number of entries which would indicate the push was not recorded (i.e. not performed?)

I think we should keep it similar to this one.
Junio C Hamano· Dec 16, 2014, 20:46 UTC · re: Stefan Beller · lore

Re: [PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

Stefan Beller <sbeller@google.com> writes:
Show 11 quoted lines
> On Tue, Dec 16, 2014 at 11:46 AM, Junio C Hamano <gitster@pobox.com> wrote:
>
>>
>> Seeing that all callers of test_ref_upstream computes $2 as
>>
>>         git -C workbench rev-parse --verify <something>
>>
>
> Only in the first tests, where this should be the case after push.
> In the failure tests, we go with HEAD@{N} which needs to be computed
> inside the workbench repo.

Aren't we saying the same thing? The suggested alternative is to do the "git -C workbench rev-parse" bit inside the helper.

Stefan Beller· Dec 16, 2014, 20:51 UTC · re: Junio C Hamano · lore

Re: [PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

On Tue, Dec 16, 2014 at 12:46 PM, Junio C Hamano <gitster@pobox.com> wrote:
Show 16 quoted lines
> Stefan Beller <sbeller@google.com> writes:
>
>> On Tue, Dec 16, 2014 at 11:46 AM, Junio C Hamano <gitster@pobox.com> wrote:
>>
>>>
>>> Seeing that all callers of test_ref_upstream computes $2 as
>>>
>>>         git -C workbench rev-parse --verify <something>
>>>
>>
>> Only in the first tests, where this should be the case after push.
>> In the failure tests, we go with HEAD@{N} which needs to be computed
>> inside the workbench repo.
>
> Aren't we saying the same thing?  The suggested alternative is to do
> the "git -C workbench rev-parse" bit inside the helper.
Now I am saying the same. (I finally understood what you were saying)
Will incorporate in a reroll.
Junio C Hamano· Dec 16, 2014, 20:30 UTC · re: Stefan Beller · lore

Re: [PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

Stefan Beller <sbeller@google.com> writes:
Show 7 quoted lines
>     > What's the value of this test?  Isn't it a non-fast-forward check
>     > you already tested in the previous one?
>     
>     I messed up there. Originally I wanted to test the 2 different
>     stages of rejection. A non-fast-forward check is done locally and
>     we don't even try pushing. But I also want to test if we locally
>     thing all is good, but the server refuses a ref to update.

It is tricky to arrange a test to fail a fast-forward check on the receiver side, because the local test is done by reading from the other side, not relying on your remote tracking branches. The usual flow is:

    pusher says to the receiver "I want to push"
    receiver says to the pusher "Here are the tips of my refs"
    pusher thinks: "Ah, I was about to update their master branch
		with this commit, but what they have is (1) not
		known to me so by definition I cannot fast-forward,
		or (2) known to me and I can definitely tell I am
		not fast-forwarding it, so I'd locally fail this
		push".

You need to invent a way to successfully race with an ongoing push. After receiver gives the tips of its refs (all of which are ancestors of what is going to be pushed) but before the pusher finishes the "thinking" above, you would somehow update the refs at the receiver so that the push will not fast-forward.

Such a raced flow would look like:
    pusher says to the receiver "I want to push"
    receiver says to the pusher "Here are the tips of my refs"
    pusher thinks: "OK, everything I'll send will fast-forward"
    pusher thinks: "Let's start generating a packfile"
    you intervene and update receiver's 'master' at this point.
    pusher send a pack and tells the receiver "I want to update your
		master from OLD to NEW".
    receiver thinks: "Huh, that OLD is not where my 'master' is"
    recevier says to the pusher "No fast-forward".
But I do not think it is practical to run such a test.

Rejecting on the receiver's end using pre-receive or update hook should be testable and should be tested.

Thanks.
Stefan Beller· Dec 16, 2014, 20:36 UTC · re: Junio C Hamano · lore

Re: [PATCHv2 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

On Tue, Dec 16, 2014 at 12:30 PM, Junio C Hamano <gitster@pobox.com> wrote:
Show 57 quoted lines
> Stefan Beller <sbeller@google.com> writes:
>
>>     > What's the value of this test?  Isn't it a non-fast-forward check
>>     > you already tested in the previous one?
>>
>>     I messed up there. Originally I wanted to test the 2 different
>>     stages of rejection. A non-fast-forward check is done locally and
>>     we don't even try pushing. But I also want to test if we locally
>>     thing all is good, but the server refuses a ref to update.
>
> It is tricky to arrange a test to fail a fast-forward check on the
> receiver side, because the local test is done by reading from the
> other side, not relying on your remote tracking branches.  The usual
> flow is:
>
>     pusher says to the receiver "I want to push"
>
>     receiver says to the pusher "Here are the tips of my refs"
>
>     pusher thinks: "Ah, I was about to update their master branch
>                 with this commit, but what they have is (1) not
>                 known to me so by definition I cannot fast-forward,
>                 or (2) known to me and I can definitely tell I am
>                 not fast-forwarding it, so I'd locally fail this
>                 push".
>
> You need to invent a way to successfully race with an ongoing push.
> After receiver gives the tips of its refs (all of which are
> ancestors of what is going to be pushed) but before the pusher
> finishes the "thinking" above, you would somehow update the refs at
> the receiver so that the push will not fast-forward.
>
> Such a raced flow would look like:
>
>     pusher says to the receiver "I want to push"
>
>     receiver says to the pusher "Here are the tips of my refs"
>
>     pusher thinks: "OK, everything I'll send will fast-forward"
>     pusher thinks: "Let's start generating a packfile"
>
>     you intervene and update receiver's 'master' at this point.
>
>     pusher send a pack and tells the receiver "I want to update your
>                 master from OLD to NEW".
>
>     receiver thinks: "Huh, that OLD is not where my 'master' is"
>
>     recevier says to the pusher "No fast-forward".
>
> But I do not think it is practical to run such a test.
>
> Rejecting on the receiver's end using pre-receive or update hook
> should be testable and should be tested.
>
> Thanks.
>
Yes, that was my understanding as well. I just messed up mentally.

Now that the update hook test is in place, we do have tests from the local and the remote side rejecting, so it should be fine now.

Junio C Hamano· Dec 16, 2014, 19:05 UTC · re: Stefan Beller · lore

Re: [PATCHv2 1/6] receive-pack.c: add protocol support to negotiate atomic-push

Stefan Beller <sbeller@google.com> writes:
Show 29 quoted lines
> diff --git a/send-pack.c b/send-pack.c
> index 949cb61..2a513f4 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -294,6 +294,8 @@ int send_pack(struct send_pack_args *args,
>  	int use_sideband = 0;
>  	int quiet_supported = 0;
>  	int agent_supported = 0;
> +	int use_atomic;
> +	int atomic_supported = 0;
>  	unsigned cmds_sent = 0;
>  	int ret;
>  	struct async demux;
> @@ -314,6 +316,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;
>  
> @@ -328,6 +332,11 @@ 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) {
> +		fprintf(stderr, "Server does not support atomic push.");
> +		return -1;
I'd tweak this to
	return error("server does not support atomic push.");

to (0) shorten, (1) make sure the message is terminated with LF, and (2) match the other error messages in the program.

Other than that looks good.
Thanks.
Show 28 quoted lines
> +	}
> +	use_atomic = atomic_supported && args->atomic;
>  
>  	if (status_report)
>  		strbuf_addstr(&cap_buf, " report-status");
> @@ -335,6 +344,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());
>  
> diff --git a/send-pack.h b/send-pack.h
> index 5635457..b664648 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,
Stefan Beller· Dec 17, 2014, 18:32 UTC · re: Junio C Hamano · lore

[PATCHv3 0/6] atomic pushes

This patch series adds a flag to git push to update the remote refs atomically.

This series applies on top of origin/mh/reflog-expire It can also be found at github[2].

Changes v3->v3 are annotated in each patch.
[2] https://github.com/stefanbeller/git/tree/atomic-push-v3
Ronnie Sahlberg (4):
  receive-pack.c: add protocol support to negotiate atomic-push
  send-pack.c: add --atomic command line argument
  receive-pack.c: use a single transaction when atomic-push is
    negotiated
  push.c: add an --atomic argument
Stefan Beller (2):
  send-pack: Rename ref_update_to_be_sent to check_to_send_update
  t5543-atomic-push.sh: add basic tests for atomic pushes
 Documentation/git-push.txt                        |   7 +-
 Documentation/git-send-pack.txt                   |   7 +-
 Documentation/technical/protocol-capabilities.txt |  13 +-
 builtin/push.c                                    |   2 +
 builtin/receive-pack.c                            |  89 +++++++++--
 builtin/send-pack.c                               |   6 +-
 remote.h                                          |   3 +-
 send-pack.c                                       |  66 +++++++-
 send-pack.h                                       |   3 +-
 t/t5543-atomic-push.sh                            | 178 ++++++++++++++++++++++
 transport.c                                       |   5 +
 transport.h                                       |   1 +
 12 files changed, 349 insertions(+), 31 deletions(-)
 create mode 100755 t/t5543-atomic-push.sh
-- 
2.2.0.31.gad78000.dirty
Stefan Beller· Dec 17, 2014, 18:32 UTC · re: Stefan Beller · lore

[PATCHv3 1/6] receive-pack.c: add protocol support to negotiate atomic

From: Ronnie Sahlberg <sahlberg@google.com>
This adds support to the protocol between send-pack and receive-pack to
* allow receive-pack to inform the client that it has atomic push capability
* allow send-pack to request atomic push back.

There is currently no setting in send-pack to actually request that atomic pushes are to be used yet. This only adds protocol capability not ability for the user to activate it.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2:
    	* Name it atomic instead of atomic-push. The name changes affects
    	  the flags send over the wire as well as the flags in
    	  struct send_pack_args
    	* Add check, which was part of the later patch here:
    	if (args->atomic && !atomic_supported) {
    		fprintf(stderr, "Server does not support atomic push.");
    		return -1;
    	}
    
    v2 -> v3:
    More concise error reporting as suggested by Junio
    -		fprintf(stderr, "Server does not support atomic push.");
    -		return -1;
    +		return error("server does not support atomic push.");
 Documentation/technical/protocol-capabilities.txt | 13 +++++++++++--
 builtin/receive-pack.c                            |  6 +++++-
 send-pack.c                                       | 10 ++++++++++
 send-pack.h                                       |  3 ++-
 4 files changed, 28 insertions(+), 4 deletions(-)
Show changes to 4 files +28 −4

Documentation/technical/protocol-capabilities.txt, builtin/receive-pack.c, send-pack.c, send-pack.h

diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 6d5424c..68ec23d 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 single atomic transaction. Either all refs are
+updated or none.
+
 allow-tip-sha1-in-want
 ----------------------
 
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 32fc540..e76e5d5 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -40,6 +40,7 @@ static int transfer_unpack_limit = -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;
@@ -171,7 +172,8 @@ static void show_ref(const char *path, const unsigned char *sha1)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status delete-refs side-band-64k quiet "
+			      "atomic");
 		if (prefer_ofs_delta)
 			strbuf_addstr(&cap, " ofs-delta");
 		if (push_cert_nonce)
@@ -1179,6 +1181,8 @@ 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 (parse_feature_request(feature_list, "atomic"))
+				use_atomic = 1;
 		}
 
 		if (!strcmp(line, "push-cert")) {
diff --git a/send-pack.c b/send-pack.c
index 949cb61..e2bdd89 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -294,6 +294,8 @@ int send_pack(struct send_pack_args *args,
 	int use_sideband = 0;
 	int quiet_supported = 0;
 	int agent_supported = 0;
+	int use_atomic;
+	int atomic_supported = 0;
 	unsigned cmds_sent = 0;
 	int ret;
 	struct async demux;
@@ -314,6 +316,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;
 
@@ -328,6 +332,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) {
+		return error("server does not support atomic push.");
+	}
+	use_atomic = atomic_supported && args->atomic;
 
 	if (status_report)
 		strbuf_addstr(&cap_buf, " report-status");
@@ -335,6 +343,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());
 
diff --git a/send-pack.h b/send-pack.h
index 5635457..b664648 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,
-- 
2.2.0.31.gad78000.dirty
Eric Sunshine· Dec 19, 2014, 01:05 UTC · re: Stefan Beller · lore

Re: [PATCHv3 1/6] receive-pack.c: add protocol support to negotiate atomic

On Wed, Dec 17, 2014 at 1:32 PM, Stefan Beller <sbeller@google.com> wrote:
Show 27 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> This adds support to the protocol between send-pack and receive-pack to
> * allow receive-pack to inform the client that it has atomic push capability
> * allow send-pack to request atomic push back.
>
> There is currently no setting in send-pack to actually request that atomic
> pushes are to be used yet. This only adds protocol capability not ability
> for the user to activate it.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
> diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
> index 6d5424c..68ec23d 100644
> --- a/Documentation/technical/protocol-capabilities.txt
> +++ b/Documentation/technical/protocol-capabilities.txt
> @@ -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 single atomic transaction. Either all refs are
"one single atomic" sounds awfully redundant.
Show 15 quoted lines
> +updated or none.
> +
>  allow-tip-sha1-in-want
>  ----------------------
>
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index 32fc540..e76e5d5 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -328,6 +332,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) {
> +               return error("server does not support atomic push.");

Just above this code, the 'args->push_cert' check uses die() rather than error() when the remote side fails to support the requested feature.

Show 5 quoted lines
> +       }
> +       use_atomic = atomic_supported && args->atomic;
>
>         if (status_report)
>                 strbuf_addstr(&cap_buf, " report-status");
Stefan Beller· Dec 17, 2014, 18:32 UTC · re: Stefan Beller · lore

[PATCHv3 2/6] send-pack: Rename ref_update_to_be_sent to check_to_send_update

This renames ref_update_to_be_sent to check_to_send_update and inverts the meaning of the return value. Having the return value inverted we can have different values for the error codes. This is useful in a later patch when we want to know if we hit the CHECK_REF_STATUS_REJECTED case.

Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    This was introduced with the [PATCHv2] series.
    Changes v2 -> v3:
    
    * Rename to check_to_send_update
    * Negative error values.
    * errors values are #define'd and not raw numbers.
 send-pack.c | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)
Show changes to send-pack.c +11 −7
diff --git a/send-pack.c b/send-pack.c
index e2bdd89..77e4201 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -190,10 +190,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) {
@@ -203,10 +206,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;
 	}
 }
 
@@ -250,7 +254,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",
@@ -369,7 +373,7 @@ 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))
+		if (check_to_send_update(ref, args) < 0)
 			continue;
 
 		if (!ref->deletion)
@@ -390,7 +394,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);
-- 
2.2.0.31.gad78000.dirty
Junio C Hamano· Dec 17, 2014, 22:53 UTC · re: Stefan Beller · lore

Re: [PATCHv3 2/6] send-pack: Rename ref_update_to_be_sent to check_to_send_update

Stefan Beller <sbeller@google.com> writes:
Show 5 quoted lines
> This renames ref_update_to_be_sent to check_to_send_update and inverts
> the meaning of the return value. Having the return value inverted we
> can have different values for the error codes. This is useful in a
> later patch when we want to know if we hit the CHECK_REF_STATUS_REJECTED
> case.

Makes sense; I would have defined these constants negative, but that can be cleaned up if somebody cares deeply enough later.

Will queue.  Thanks.
Show 76 quoted lines
>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>
> Notes:
>     This was introduced with the [PATCHv2] series.
>     Changes v2 -> v3:
>     
>     * Rename to check_to_send_update
>     * Negative error values.
>     * errors values are #define'd and not raw numbers.
>
>  send-pack.c | 18 +++++++++++-------
>  1 file changed, 11 insertions(+), 7 deletions(-)
>
> diff --git a/send-pack.c b/send-pack.c
> index e2bdd89..77e4201 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -190,10 +190,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) {
> @@ -203,10 +206,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;
>  	}
>  }
>  
> @@ -250,7 +254,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",
> @@ -369,7 +373,7 @@ 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))
> +		if (check_to_send_update(ref, args) < 0)
>  			continue;
>  
>  		if (!ref->deletion)
> @@ -390,7 +394,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);
Stefan Beller· Dec 17, 2014, 18:32 UTC · re: Stefan Beller · lore

[PATCHv3 3/6] send-pack.c: add --atomic command line argument

From: Ronnie Sahlberg <sahlberg@google.com>

This adds support to send-pack to negotiate and use atomic pushes iff the server supports it. Atomic pushes are activated by a new command line flag --atomic.

In order to do this we also need to change the semantics for send_pack() slightly. The existing send_pack() function actually doesn't send all the refs back to the server when multiple refs are involved, for example when using --all. Several of the failure modes for pushes can already be detected locally in the send_pack client based on the information from the initial server side list of all the refs as generated by receive-pack. Any such refs that we thus know would fail to push are thus pruned from the list of refs we send to the server to update.

For atomic pushes, we have to deal thus with both failures that are detected locally as well as failures that are reported back from the server. In order to do so we treat all local failures as push failures too.

We introduce a new status code REF_STATUS_ATOMIC_PUSH_FAILED so we can flag all refs that we would normally have tried to push to the server but we did not due to local failures. This is to improve the error message back to the end user to flag that "these refs failed to update since the atomic push operation failed."

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Notes:
        Changes v1 -> v2:
         * Now we only need a very small change in the existing code and have
           a new static function, which cares about error reporting.
              Junio wrote:
              > Hmph.  Is "atomic push" so special that it deserves a separate
              > parameter?  When we come up with yet another mode of failure, would
              > we add another parameter to the callers to this function?
         * error messages are worded differently (lower case!),
         * use of error function instead of fprintf
    
         * undashed the printed error message ("atomic push failed");
    
    Changes v2 -> v3:
    > We avoid assignment inside a conditional.
    
    Ok I switched to using a switch statement.
 Documentation/git-send-pack.txt |  7 ++++++-
 builtin/send-pack.c             |  6 +++++-
 remote.h                        |  3 ++-
 send-pack.c                     | 40 ++++++++++++++++++++++++++++++++++++++--
 transport.c                     |  4 ++++
 5 files changed, 55 insertions(+), 5 deletions(-)
Show changes to 5 files +55 −5

Documentation/git-send-pack.txt, builtin/send-pack.c, remote.h, send-pack.c, transport.c

diff --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt
index 2a0de42..45c7725 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/builtin/send-pack.c b/builtin/send-pack.c
index b564a77..b961e5a 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 8b62efd..f346524 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 77e4201..a696d5c 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -282,6 +282,30 @@ 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:
+			; /* do nothing */
+		}
+	}
+	error("atomic push failed for ref %s. "
+	      "status: %d\n", failing_ref->name, failing_ref->status);
+	return -1;
+}
+
 int send_pack(struct send_pack_args *args,
 	      int fd[], struct child_process *conn,
 	      struct ref *remote_refs,
@@ -373,9 +397,21 @@ int send_pack(struct send_pack_args *args,
 	 * the pack data.
 	 */
 	for (ref = remote_refs; ref; ref = ref->next) {
-		if (check_to_send_update(ref, args) < 0)
+		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;
 
diff --git a/transport.c b/transport.c
index 70d38e4..c67feee 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;
-- 
2.2.0.31.gad78000.dirty
Junio C Hamano· Dec 17, 2014, 23:14 UTC · re: Stefan Beller · lore

Re: [PATCHv3 3/6] send-pack.c: add --atomic command line argument

Stefan Beller <sbeller@google.com> writes:
Show 5 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> This adds support to send-pack to negotiate and use atomic pushes
> iff the server supports it. Atomic pushes are activated by a new command
> line flag --atomic.
Looks sensible.  Thanks.
Eric Sunshine· Dec 19, 2014, 01:22 UTC · re: Stefan Beller · lore

Re: [PATCHv3 3/6] send-pack.c: add --atomic command line argument

On Wed, Dec 17, 2014 at 1:32 PM, Stefan Beller <sbeller@google.com> wrote:
Show 39 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> This adds support to send-pack to negotiate and use atomic pushes
> iff the server supports it. Atomic pushes are activated by a new command
> line flag --atomic.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
> diff --git a/send-pack.c b/send-pack.c
> index 77e4201..a696d5c 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -282,6 +282,30 @@ 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:
> +                       ; /* do nothing */
> +               }
> +       }
> +       error("atomic push failed for ref %s. "
> +             "status: %d\n", failing_ref->name, failing_ref->status);
> +       return -1;
error() returns -1, so:
    return error(...);

Also, why split the string literal like that? It's not warranted by the indentation level. Instead:

    return error("atomic push failed for ref %s. status: %d\n",
        failing_ref->name, failing_ref->status);
Show 14 quoted lines
> +}
> +
>  int send_pack(struct send_pack_args *args,
>               int fd[], struct child_process *conn,
>               struct ref *remote_refs,
> @@ -373,9 +397,21 @@ int send_pack(struct send_pack_args *args,
>          * the pack data.
>          */
>         for (ref = remote_refs; ref; ref = ref->next) {
> -               if (check_to_send_update(ref, args) < 0)
> +               switch (check_to_send_update(ref, args)) {
> +               case 0: /* no error */
> +                       break;
> +               case -CHECK_REF_STATUS_REJECTED:

I realize that Junio said that this could wait for cleanup patch by someone later, but I'd argue that defining these constants now with their proper negative values would be beneficial:

First, it is a potential maintenance burden that each person working on the code later must remember to negate the constants each time they are used. Forgetting to do so will lead to incorrect behavior.

Second, negating these constants at point-of-use implies incorrectly that there is some valid use-case for the non-negated forms. It's misleading and confusing.

Show 14 quoted lines
> +                       /*
> +                        * 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;
Stefan Beller· Dec 17, 2014, 18:32 UTC · re: Stefan Beller · lore

[PATCHv3 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

From: Ronnie Sahlberg <sahlberg@google.com>

Update receive-pack to use an atomic transaction iff the client negotiated that it wanted atomic-push. This leaves the default behavior to be the old non-atomic one ref at a time update. This is to cause as little disruption as possible to existing clients. It is unknown if there are client scripts that depend on the old non-atomic behavior so we make it opt-in for now.

If it turns out over time that there are no client scripts that depend on the old behavior we can change git to default to use atomic pushes and instead offer an opt-out argument for people that do not want atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2:
    	* update(...) assumes to be always in a transaction
    	* Caring about when to begin/commit transactions is put
    	  into execute_commands
    v2->v3:
    	* meditated about the error flow. Now we always construct a local
    	  strbuf err if required. Then the flow is easier to follow and
    	  destruction of it is performed nearby.
    	* early return in execute_commands if transaction_begin fails.
 builtin/receive-pack.c | 83 +++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 68 insertions(+), 15 deletions(-)
Show changes to builtin/receive-pack.c +68 −15
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e76e5d5..c942a3b 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -67,6 +67,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)
 {
@@ -823,6 +824,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)) {
@@ -832,35 +834,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 */
 	}
 }
@@ -1052,6 +1055,7 @@ static void execute_commands(struct command *commands,
 	struct command *cmd;
 	unsigned char sha1[20];
 	struct iterate_data data;
+	struct strbuf err = STRBUF_INIT;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1059,6 +1063,16 @@ static void execute_commands(struct command *commands,
 		return;
 	}
 
+	if (use_atomic) {
+		transaction = ref_transaction_begin(&err);
+		if (!transaction) {
+			error("%s", err.buf);
+			strbuf_release(&err);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = "transaction error";
+			return;
+		}
+	}
 	data.cmds = commands;
 	data.si = si;
 	if (check_everything_connected(iterate_receive_command_list, 0, &data))
@@ -1086,8 +1100,25 @@ static void execute_commands(struct command *commands,
 
 		if (cmd->skip_update)
 			continue;
-
+		if (!use_atomic) {
+			transaction = ref_transaction_begin(&err);
+			if (!transaction) {
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				cmd->error_string = "failed to start transaction";
+				return;
+			}
+		}
 		cmd->error_string = update(cmd, si);
+		if (!use_atomic)
+			if (ref_transaction_commit(transaction, &err)) {
+				ref_transaction_free(transaction);
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				cmd->error_string = "failed to update ref";
+				return;
+			}
+
 		if (shallow_update && !cmd->error_string &&
 		    si->shallow_ref[cmd->index]) {
 			error("BUG: connectivity check has not been run on ref %s",
@@ -1096,10 +1127,32 @@ static void execute_commands(struct command *commands,
 		}
 	}
 
+	if (use_atomic) {
+		/*
+		 * update(...) may abort early (i.e. because the hook refused to
+		 * update that ref) which then doesn't even record a transaction
+		 * regarding that ref. Make sure all commands are without error
+		 * and then commit atomically.
+		 */
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (cmd->error_string)
+				break;
+		if (cmd) {
+			for (cmd = commands; cmd; cmd = cmd->next)
+				if (!cmd->error_string)
+					cmd->error_string = "atomic push failure";
+		} else if (ref_transaction_commit(transaction, &err)) {
+			rp_error("%s", err.buf);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = err.buf;
+		}
+		ref_transaction_free(transaction);
+	}
 	if (shallow_update && !checked_connectivity)
 		error("BUG: run 'git fsck' for safety.\n"
 		      "If there are errors, try to remove "
 		      "the reported refs above");
+	strbuf_release(&err);
 }
 
 static struct command **queue_command(struct command **tail,
-- 
2.2.0.31.gad78000.dirty
Junio C Hamano· Dec 17, 2014, 23:26 UTC · re: Stefan Beller · lore

Re: [PATCHv3 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

Stefan Beller <sbeller@google.com> writes:
Show 23 quoted lines
> @@ -1086,8 +1100,25 @@ static void execute_commands(struct command *commands,
>  
>  		if (cmd->skip_update)
>  			continue;
> -
> +		if (!use_atomic) {
> +			transaction = ref_transaction_begin(&err);
> +			if (!transaction) {
> +				rp_error("%s", err.buf);
> +				strbuf_release(&err);
> +				cmd->error_string = "failed to start transaction";
> +				return;
> +			}
> +		}
>  		cmd->error_string = update(cmd, si);
> +		if (!use_atomic)
> +			if (ref_transaction_commit(transaction, &err)) {
> +				ref_transaction_free(transaction);
> +				rp_error("%s", err.buf);
> +				strbuf_release(&err);
> +				cmd->error_string = "failed to update ref";
> +				return;
> +			}

Hmm, should the code even attempt to commit if update() returned a non NULL, signaling a failure?

Or would we want to do this instead?
	if (cmd->error_string)
        	goto transaction_abort;
	else if (!use_atomic) {
		if (ref_transaction_commit(...)) {
			...
                        cmd->error_string = "...";
                        return;
        	}
	}
and then ...
Show 26 quoted lines
>  		if (shallow_update && !cmd->error_string &&
>  		    si->shallow_ref[cmd->index]) {
>  			error("BUG: connectivity check has not been run on ref %s",
> @@ -1096,10 +1127,32 @@ static void execute_commands(struct command *commands,
>  		}
>  	}
>  
> +	if (use_atomic) {
> +		/*
> +		 * update(...) may abort early (i.e. because the hook refused to
> +		 * update that ref) which then doesn't even record a transaction
> +		 * regarding that ref. Make sure all commands are without error
> +		 * and then commit atomically.
> +		 */
> +		for (cmd = commands; cmd; cmd = cmd->next)
> +			if (cmd->error_string)
> +				break;
> +		if (cmd) {
> +			for (cmd = commands; cmd; cmd = cmd->next)
> +				if (!cmd->error_string)
> +					cmd->error_string = "atomic push failure";
> +		} else if (ref_transaction_commit(transaction, &err)) {
> +			rp_error("%s", err.buf);
> +			for (cmd = commands; cmd; cmd = cmd->next)
> +				cmd->error_string = err.buf;
> +		}
... have the label to jump to here:
	transaction_abort:
> +		ref_transaction_free(transaction);

I was confused by the fact that you did not have any call to transaction-abort, until I realized that there is no such API function and ref_transaction_free() serves that "don't commit, roll it back" purpose.

Show 9 quoted lines
> +	}
>  	if (shallow_update && !checked_connectivity)
>  		error("BUG: run 'git fsck' for safety.\n"
>  		      "If there are errors, try to remove "
>  		      "the reported refs above");
> +	strbuf_release(&err);
>  }
>  
>  static struct command **queue_command(struct command **tail,
Stefan Beller· Dec 17, 2014, 23:58 UTC · re: Junio C Hamano · lore

Re: [PATCHv3 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

On Wed, Dec 17, 2014 at 3:26 PM, Junio C Hamano <gitster@pobox.com> wrote:
Show 30 quoted lines
> Stefan Beller <sbeller@google.com> writes:
>
>> @@ -1086,8 +1100,25 @@ static void execute_commands(struct command *commands,
>>
>>               if (cmd->skip_update)
>>                       continue;
>> -
>> +             if (!use_atomic) {
>> +                     transaction = ref_transaction_begin(&err);
>> +                     if (!transaction) {
>> +                             rp_error("%s", err.buf);
>> +                             strbuf_release(&err);
>> +                             cmd->error_string = "failed to start transaction";
>> +                             return;
>> +                     }
>> +             }
>>               cmd->error_string = update(cmd, si);
>> +             if (!use_atomic)
>> +                     if (ref_transaction_commit(transaction, &err)) {
>> +                             ref_transaction_free(transaction);
>> +                             rp_error("%s", err.buf);
>> +                             strbuf_release(&err);
>> +                             cmd->error_string = "failed to update ref";
>> +                             return;
>> +                     }
>
> Hmm, should the code even attempt to commit if update() returned a
> non NULL, signaling a failure?
>
> Or would we want to do this instead?

This would change the current behavior. In the case of !atomic we want to consider all commands and not stop early.

So maybe more
if (!cmd->error_string) {
        if (!use_atomic
            && ref_transaction_commit(...)) {
            ...
        }
} else {
        if (use_atomic)
             goto check_atomic_commit;
}
and the  check_atomic_commit label is replacing the loop to check:
-        for (cmd = commands; cmd; cmd = cmd->next)
-                if (cmd->error_string)
-                        break;
+ check_atomic_commit:
Junio C Hamano· Dec 18, 2014, 17:02 UTC · re: Stefan Beller · lore

Re: [PATCHv3 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

Stefan Beller <sbeller@google.com> writes:
> This would change the current behavior. In the case of !atomic we want
> to consider all commands and not stop early.
Quite right.
Show 10 quoted lines
> So maybe more
> if (!cmd->error_string) {
>         if (!use_atomic
>             && ref_transaction_commit(...)) {
>             ...
>         }
> } else {
>         if (use_atomic)
>              goto check_atomic_commit;
> }

Don't you need to rollback/abort/free in one-at-a-time mode and did not commit because error_string is already set, though?

Stefan Beller· Dec 18, 2014, 17:45 UTC · re: Junio C Hamano · lore

[PATCHv4 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

From: Ronnie Sahlberg <sahlberg@google.com>

Update receive-pack to use an atomic transaction iff the client negotiated that it wanted atomic-push. This leaves the default behavior to be the old non-atomic one ref at a time update. This is to cause as little disruption as possible to existing clients. It is unknown if there are client scripts that depend on the old non-atomic behavior so we make it opt-in for now.

If it turns out over time that there are no client scripts that depend on the old behavior we can change git to default to use atomic pushes and instead offer an opt-out argument for people that do not want atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
v4 of the series only resends this patch as the others did not get any 
feedback to improve.
        
Notes:
    Changes v1 -> v2:
    	* update(...) assumes to be always in a transaction
    	* Caring about when to begin/commit transactions is put
    	  into execute_commands
    v2->v3:
    	* meditated about the error flow. Now we always construct a local
    	  strbuf err if required. Then the flow is easier to follow and
    	  destruction of it is performed nearby.
    	* early return in execute_commands if transaction_begin fails.
    
    v3->v4:
    	* revamp logic again. This should keep the non atomic behavior
    	  as is (in case of error say so, in non error case just free the
    	  transaction). In the atomic case we either do nothing (when no error),
    	  or abort with the goto.
    
    		if (!cmd->error_string) {
    			if (!use_atomic
    			    && ref_transaction_commit(transaction, &err)) {
    				ref_transaction_free(transaction);
    				rp_error("%s", err.buf);
    				strbuf_release(&err);
    				cmd->error_string = "failed to update ref";
    			}
    		} else if (use_atomic) {
    			goto atomic_failure;
    		} else {
    			ref_transaction_free(transaction);
    		}
    
    	 * Having the goto directly there when checking for cmd->error_string,
    	   we don't need to do it again, so the paragraph explaining the error
    	   checking is gone as well. (Previous patch had the following, this is
    	   put at the end of the function, where the goto jumps to and the comment
    	   has been dropped.
    +		/*
    +		 * update(...) may abort early (i.e. because the hook refused to
    +		 * update that ref) which then doesn't even record a transaction
    +		 * regarding that ref. Make sure all commands are without error
    +		 * and then commit atomically.
    +		 */
    +		for (cmd = commands; cmd; cmd = cmd->next)
    +			if (cmd->error_string)
    +				break;
 builtin/receive-pack.c | 83 +++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 69 insertions(+), 14 deletions(-)
Show changes to builtin/receive-pack.c +69 −14
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e76e5d5..4952d91 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -67,6 +67,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)
 {
@@ -823,6 +824,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)) {
@@ -832,35 +834,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 */
 	}
 }
@@ -1052,6 +1055,7 @@ static void execute_commands(struct command *commands,
 	struct command *cmd;
 	unsigned char sha1[20];
 	struct iterate_data data;
+	struct strbuf err = STRBUF_INIT;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1059,6 +1063,16 @@ static void execute_commands(struct command *commands,
 		return;
 	}
 
+	if (use_atomic) {
+		transaction = ref_transaction_begin(&err);
+		if (!transaction) {
+			error("%s", err.buf);
+			strbuf_release(&err);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = "transaction error";
+			return;
+		}
+	}
 	data.cmds = commands;
 	data.si = si;
 	if (check_everything_connected(iterate_receive_command_list, 0, &data))
@@ -1087,7 +1101,30 @@ static void execute_commands(struct command *commands,
 		if (cmd->skip_update)
 			continue;
 
+		if (!use_atomic) {
+			transaction = ref_transaction_begin(&err);
+			if (!transaction) {
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				cmd->error_string = "failed to start transaction";
+				continue;
+			}
+		}
 		cmd->error_string = update(cmd, si);
+		if (!cmd->error_string) {
+			if (!use_atomic
+			    && ref_transaction_commit(transaction, &err)) {
+				ref_transaction_free(transaction);
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				cmd->error_string = "failed to update ref";
+			}
+		} else if (use_atomic) {
+			goto atomic_failure;
+		} else {
+			ref_transaction_free(transaction);
+		}
+
 		if (shallow_update && !cmd->error_string &&
 		    si->shallow_ref[cmd->index]) {
 			error("BUG: connectivity check has not been run on ref %s",
@@ -1096,10 +1133,28 @@ static void execute_commands(struct command *commands,
 		}
 	}
 
+	if (use_atomic) {
+		if (ref_transaction_commit(transaction, &err)) {
+			rp_error("%s", err.buf);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = err.buf;
+		}
+		ref_transaction_free(transaction);
+	}
 	if (shallow_update && !checked_connectivity)
 		error("BUG: run 'git fsck' for safety.\n"
 		      "If there are errors, try to remove "
 		      "the reported refs above");
+	strbuf_release(&err);
+
+	return;
+
+atomic_failure:
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (!cmd->error_string)
+			cmd->error_string = "atomic push failure";
+
+	ref_transaction_free(transaction);
 }
 
 static struct command **queue_command(struct command **tail,
-- 
2.2.0.31.gad78000.dirty
Eric Sunshine· Dec 18, 2014, 22:26 UTC · re: Stefan Beller · lore

Re: [PATCHv4 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

On Thu, Dec 18, 2014 at 12:45 PM, Stefan Beller <sbeller@google.com> wrote:
Show 25 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> Update receive-pack to use an atomic transaction iff the client negotiated
> that it wanted atomic-push. This leaves the default behavior to be the old
> non-atomic one ref at a time update. This is to cause as little disruption
> as possible to existing clients. It is unknown if there are client scripts
> that depend on the old non-atomic behavior so we make it opt-in for now.
>
> If it turns out over time that there are no client scripts that depend on the
> old behavior we can change git to default to use atomic pushes and instead
> offer an opt-out argument for people that do not want atomic pushes.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index e76e5d5..4952d91 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -1059,6 +1063,16 @@ static void execute_commands(struct command *commands,
>                 return;
>         }
>
> +       if (use_atomic) {
> +               transaction = ref_transaction_begin(&err);

Repeating from my earlier review[1]: If the 'pre-receive' hook "declines", then this transaction is left dangling (and its resources leaked).

Show 5 quoted lines
> +               if (!transaction) {
> +                       error("%s", err.buf);
> +                       strbuf_release(&err);
> +                       for (cmd = commands; cmd; cmd = cmd->next)
> +                               cmd->error_string = "transaction error";

The !use_atomic case (below), calls this error "failed to start transaction", not merely "transaction error".

Furthermore, in the use_atomic case (also below), when a commit fails, you assign err.buf to cmd->error_string rather than a generic "transaction error" message. What differs between these cases which makes the generic message preferable here over the more specific err.buf message?

Show 24 quoted lines
> +                       return;
> +               }
> +       }
>         data.cmds = commands;
>         data.si = si;
>         if (check_everything_connected(iterate_receive_command_list, 0, &data))
> @@ -1087,7 +1101,30 @@ static void execute_commands(struct command *commands,
>                 if (cmd->skip_update)
>                         continue;
>
> +               if (!use_atomic) {
> +                       transaction = ref_transaction_begin(&err);
> +                       if (!transaction) {
> +                               rp_error("%s", err.buf);
> +                               strbuf_release(&err);
> +                               cmd->error_string = "failed to start transaction";
> +                               continue;
> +                       }
> +               }
>                 cmd->error_string = update(cmd, si);
> +               if (!cmd->error_string) {
> +                       if (!use_atomic
> +                           && ref_transaction_commit(transaction, &err)) {
> +                               ref_transaction_free(transaction);

Repeating from my earlier review[1]: This is leaking 'transaction' for each successful commit (and only freeing it upon commit error).

Show 6 quoted lines
> +                               rp_error("%s", err.buf);
> +                               strbuf_release(&err);
> +                               cmd->error_string = "failed to update ref";
> +                       }
> +               } else if (use_atomic) {
> +                       goto atomic_failure;
See commentary below.
Show 16 quoted lines
> +               } else {
> +                       ref_transaction_free(transaction);
> +               }
> +
>                 if (shallow_update && !cmd->error_string &&
>                     si->shallow_ref[cmd->index]) {
>                         error("BUG: connectivity check has not been run on ref %s",
> @@ -1096,10 +1133,28 @@ static void execute_commands(struct command *commands,
>                 }
>         }
>
> +       if (use_atomic) {
> +               if (ref_transaction_commit(transaction, &err)) {
> +                       rp_error("%s", err.buf);
> +                       for (cmd = commands; cmd; cmd = cmd->next)
> +                               cmd->error_string = err.buf;

At the end of this function, strbuf_release(&err) is invoked, which leaves all these cmd->error_strings dangling.

Show 17 quoted lines
> +               }
> +               ref_transaction_free(transaction);
> +       }
>         if (shallow_update && !checked_connectivity)
>                 error("BUG: run 'git fsck' for safety.\n"
>                       "If there are errors, try to remove "
>                       "the reported refs above");
> +       strbuf_release(&err);
> +
> +       return;
> +
> +atomic_failure:
> +       for (cmd = commands; cmd; cmd = cmd->next)
> +               if (!cmd->error_string)
> +                       cmd->error_string = "atomic push failure";
> +
> +       ref_transaction_free(transaction);

goto's can help simplify error-handling when multiple conditional branches need to perform common cleanup, however, this label corresponds to only a single goto statement. Placing the cleanup code for that one case so far away from the point where the condition is detected actually obfuscates the code slightly rather than clarifying it. It would be a bit easier to understand the logic if this cleanup (plus a 'return') was inlined directly at the location of the 'goto'.

By the way, you employ the idiom of iterating over 'commands' and assigning error_string often enough, that it might be worthwhile to wrap it in a function. In that case, inlining the above cleanup code in place of the 'goto' would make it even easier to read since it would be a mere two-liner.

>  }
[1]: http://article.gmane.org/gmane.comp.version-control.git/261455
Stefan Beller· Dec 19, 2014, 00:22 UTC · re: Eric Sunshine · lore

[PATCHv5 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

From: Ronnie Sahlberg <sahlberg@google.com>

Update receive-pack to use an atomic transaction iff the client negotiated that it wanted atomic-push. This leaves the default behavior to be the old non-atomic one ref at a time update. This is to cause as little disruption as possible to existing clients. It is unknown if there are client scripts that depend on the old non-atomic behavior so we make it opt-in for now.

If it turns out over time that there are no client scripts that depend on the old behavior we can change git to default to use atomic pushes and instead offer an opt-out argument for people that do not want atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2:
    	* update(...) assumes to be always in a transaction
    	* Caring about when to begin/commit transactions is put
    	  into execute_commands
    v2->v3:
    	* meditated about the error flow. Now we always construct a local
    	  strbuf err if required. Then the flow is easier to follow and
    	  destruction of it is performed nearby.
    	* early return in execute_commands if transaction_begin fails.
    
    v3->v4:
    	* revamp logic again. This should keep the non atomic behavior
    	  as is (in case of error say so, in non error case just free the
    	  transaction). In the atomic case we either do nothing (when no error),
    	  or abort with the goto.
    
    		if (!cmd->error_string) {
    			if (!use_atomic
    			    && ref_transaction_commit(transaction, &err)) {
    				ref_transaction_free(transaction);
    				rp_error("%s", err.buf);
    				strbuf_release(&err);
    				cmd->error_string = "failed to update ref";
    			}
    		} else if (use_atomic) {
    			goto atomic_failure;
    		} else {
    			ref_transaction_free(transaction);
    		}
    
    	 * Having the goto directly there when checking for cmd->error_string,
    	   we don't need to do it again, so the paragraph explaining the error
    	   checking is gone as well. (Previous patch had the following, this is
    	   put at the end of the function, where the goto jumps to and the comment
    	   has been dropped.
    +		/*
    +		 * update(...) may abort early (i.e. because the hook refused to
    +		 * update that ref) which then doesn't even record a transaction
    +		 * regarding that ref. Make sure all commands are without error
    +		 * and then commit atomically.
    +		 */
    +		for (cmd = commands; cmd; cmd = cmd->next)
    +			if (cmd->error_string)
    +				break;
    
    v4->v5:
    Eric wrote:
    > Repeating from my earlier review[1]: If the 'pre-receive' hook
    > "declines", then this transaction is left dangling (and its resources
    > leaked).
    
    You're right. The initialization of the transaction is now
    near the actual loop after the pre receive hook.
    
    > The !use_atomic case (below), calls this error "failed to start
    > transaction", not merely "transaction error".
    
    ok, now both are "transaction failed to start".
    In all cases where these generic errors are reported,
    we do have a rp_error(...) with details.
    
    > Furthermore, in the use_atomic case (also below), when a commit fails,
    > you assign err.buf to cmd->error_string rather than a generic
    > "transaction error" message. What differs between these cases which
    > makes the generic message preferable here over the more specific
    > err.buf message?
    
    They are the same now.
    
    > Repeating from my earlier review[1]: This is leaking 'transaction' for
    > each successful commit (and only freeing it upon commit error).
    
    Right. I thought I had it covered with the else clause. Of course not.
    
    > At the end of this function, strbuf_release(&err) is invoked, which
    > leaves all these cmd->error_strings dangling.
    
    I removed all assignments of err.buf now.
    
    > goto's can help simplify error-handling when multiple conditional
    > branches need to perform common cleanup, however, this label
    > corresponds to only a single goto statement.
    
    moved up again.
 builtin/receive-pack.c | 81 +++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 67 insertions(+), 14 deletions(-)
Show changes to builtin/receive-pack.c +67 −14
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e76e5d5..ebce2fa 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -67,6 +67,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)
 {
@@ -823,6 +824,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)) {
@@ -832,35 +834,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 */
 	}
 }
@@ -1052,6 +1055,7 @@ static void execute_commands(struct command *commands,
 	struct command *cmd;
 	unsigned char sha1[20];
 	struct iterate_data data;
+	struct strbuf err = STRBUF_INIT;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1080,6 +1084,17 @@ static void execute_commands(struct command *commands,
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, sha1, NULL);
 
 	checked_connectivity = 1;
+
+	if (use_atomic) {
+		transaction = ref_transaction_begin(&err);
+		if (!transaction) {
+			rp_error("%s", err.buf);
+			strbuf_release(&err);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = "transaction failed to start";
+			return;
+		}
+	}
 	for (cmd = commands; cmd; cmd = cmd->next) {
 		if (cmd->error_string)
 			continue;
@@ -1087,7 +1102,36 @@ static void execute_commands(struct command *commands,
 		if (cmd->skip_update)
 			continue;
 
+		if (!use_atomic) {
+			transaction = ref_transaction_begin(&err);
+			if (!transaction) {
+				rp_error("%s", err.buf);
+				strbuf_release(&err);
+				cmd->error_string = "transaction failed to start";
+				continue;
+			}
+		}
 		cmd->error_string = update(cmd, si);
+		if (!cmd->error_string) {
+			if (!use_atomic) {
+				if (ref_transaction_commit(transaction, &err)) {
+					rp_error("%s", err.buf);
+					strbuf_release(&err);
+					cmd->error_string = "failed to update ref";
+				}
+				ref_transaction_free(transaction);
+			}
+		} else {
+			ref_transaction_free(transaction);
+			if (use_atomic) {
+				for (cmd = commands; cmd; cmd = cmd->next)
+					if (!cmd->error_string)
+						cmd->error_string = "atomic push failure";
+				strbuf_release(&err);
+				return;
+			}
+		}
+
 		if (shallow_update && !cmd->error_string &&
 		    si->shallow_ref[cmd->index]) {
 			error("BUG: connectivity check has not been run on ref %s",
@@ -1096,10 +1140,19 @@ static void execute_commands(struct command *commands,
 		}
 	}
 
+	if (use_atomic) {
+		if (ref_transaction_commit(transaction, &err)) {
+			rp_error("%s", err.buf);
+			for (cmd = commands; cmd; cmd = cmd->next)
+				cmd->error_string = "atomic transaction failed";
+		}
+		ref_transaction_free(transaction);
+	}
 	if (shallow_update && !checked_connectivity)
 		error("BUG: run 'git fsck' for safety.\n"
 		      "If there are errors, try to remove "
 		      "the reported refs above");
+	strbuf_release(&err);
 }
 
 static struct command **queue_command(struct command **tail,
-- 
2.2.1.62.g3f15098
Eric Sunshine· Dec 19, 2014, 10:14 UTC · re: Stefan Beller · lore

Re: [PATCHv5 4/6] receive-pack.c: use a single ref_transaction for atomic pushes

On Thu, Dec 18, 2014 at 7:22 PM, Stefan Beller <sbeller@google.com> wrote:
Show 30 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> Update receive-pack to use an atomic transaction iff the client negotiated
> that it wanted atomic-push. This leaves the default behavior to be the old
> non-atomic one ref at a time update. This is to cause as little disruption
> as possible to existing clients. It is unknown if there are client scripts
> that depend on the old non-atomic behavior so we make it opt-in for now.
>
> If it turns out over time that there are no client scripts that depend on the
> old behavior we can change git to default to use atomic pushes and instead
> offer an opt-out argument for people that do not want atomic pushes.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index e76e5d5..ebce2fa 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -1087,7 +1102,36 @@ static void execute_commands(struct command *commands,
>                 if (cmd->skip_update)
>                         continue;
>
> +               if (!use_atomic) {
> +                       transaction = ref_transaction_begin(&err);
> +                       if (!transaction) {
> +                               rp_error("%s", err.buf);
> +                               strbuf_release(&err);
> +                               cmd->error_string = "transaction failed to start";
> +                               continue;
For this failure, you 'continue' the loop.
Show 9 quoted lines
> +                       }
> +               }
>                 cmd->error_string = update(cmd, si);
> +               if (!cmd->error_string) {
> +                       if (!use_atomic) {
> +                               if (ref_transaction_commit(transaction, &err)) {
> +                                       rp_error("%s", err.buf);
> +                                       strbuf_release(&err);
> +                                       cmd->error_string = "failed to update ref";
However, for this failure, you fall through...
Show 15 quoted lines
> +                               }
> +                               ref_transaction_free(transaction);
> +                       }
> +               } else {
> +                       ref_transaction_free(transaction);
> +                       if (use_atomic) {
> +                               for (cmd = commands; cmd; cmd = cmd->next)
> +                                       if (!cmd->error_string)
> +                                               cmd->error_string = "atomic push failure";
> +                               strbuf_release(&err);
> +                               return;
> +                       }
> +               }
> +
>                 if (shallow_update && !cmd->error_string &&
And here must check if an error occurred in some code above.

This is ugly and inconsistent, and feels as if the new code was plopped into the middle of this function without concern for overall flow, thus negatively impacting maintainability and readability. It could be made a bit cleaner by either (1) consistently using 'continue' for the non-atomic error cases, or (2) moving the "shallow" handling up into the conditional where you _know_ that no error has occurred (error_string is NULL).

However, this issue is symptomatic of a larger problem. Prior to this patch, execute_commands() was relatively straight-forward and easy to read and understand. With the patch, and its interleaved atomic and non-atomic cases, the logic flow is a mess: it places a high cognitive load on the reader and is difficult to maintain and to do correctly in the first place (as already evidenced).

Have you considered refactoring the code to implement the atomic and non-atomic cases as distinct single-purpose helper functions of execute_commands()? It should be possible to do so with very little duplicated code between the two functions, and the result would likely be much more readable and maintainable.

Show 24 quoted lines
>                     si->shallow_ref[cmd->index]) {
>                         error("BUG: connectivity check has not been run on ref %s",
> @@ -1096,10 +1140,19 @@ static void execute_commands(struct command *commands,
>                 }
>         }
>
> +       if (use_atomic) {
> +               if (ref_transaction_commit(transaction, &err)) {
> +                       rp_error("%s", err.buf);
> +                       for (cmd = commands; cmd; cmd = cmd->next)
> +                               cmd->error_string = "atomic transaction failed";
> +               }
> +               ref_transaction_free(transaction);
> +       }
>         if (shallow_update && !checked_connectivity)
>                 error("BUG: run 'git fsck' for safety.\n"
>                       "If there are errors, try to remove "
>                       "the reported refs above");
> +       strbuf_release(&err);
>  }
>
>  static struct command **queue_command(struct command **tail,
> --
> 2.2.1.62.g3f15098
Stefan Beller· Dec 17, 2014, 18:32 UTC · re: Stefan Beller · lore

[PATCHv3 5/6] push.c: add an --atomic argument

From: Ronnie Sahlberg <sahlberg@google.com>

Add a command line argument to the git push command to request atomic pushes.

Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes v1 -> v2
    	It's --atomic now! (dropping the -push)
    
    v2->v3:
    	* s/atomic-push/atomic/
    	* s/the an/an/
    	* no serverside, but just remote instead
    	* TRANSPORT_PUSH_ATOMIC instead of TRANSPORT_ATOMIC_PUSH
 Documentation/git-push.txt | 7 ++++++-
 builtin/push.c             | 2 ++
 transport.c                | 1 +
 transport.h                | 1 +
 4 files changed, 10 insertions(+), 1 deletion(-)
Show changes to 4 files +10 −1

Documentation/git-push.txt, builtin/push.c, transport.c, transport.h

diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
index 21b3f29..da63bdf 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.
 
+--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/builtin/push.c b/builtin/push.c
index a076b19..5731a0d 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -518,6 +518,8 @@ 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_BIT(0, "atomic", &flags, N_("use an atomic transaction remote"),
+			TRANSPORT_PUSH_ATOMIC),
 		OPT_END()
 	};
 
diff --git a/transport.c b/transport.c
index c67feee..1373152 100644
--- a/transport.c
+++ b/transport.c
@@ -830,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 3e0091e..18d2cf8 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)
-- 
2.2.0.31.gad78000.dirty
Eric Sunshine· Dec 19, 2014, 01:29 UTC · re: Stefan Beller · lore

Re: [PATCHv3 5/6] push.c: add an --atomic argument

On Wed, Dec 17, 2014 at 1:32 PM, Stefan Beller <sbeller@google.com> wrote:
Show 17 quoted lines
> From: Ronnie Sahlberg <sahlberg@google.com>
>
> Add a command line argument to the git push command to request atomic
> pushes.
>
> Signed-off-by: Ronnie Sahlberg <sahlberg@google.com>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
> diff --git a/builtin/push.c b/builtin/push.c
> index a076b19..5731a0d 100644
> --- a/builtin/push.c
> +++ b/builtin/push.c
> @@ -518,6 +518,8 @@ 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_BIT(0, "atomic", &flags, N_("use an atomic transaction remote"),
Unable to parse. Perhaps:
    "use an atomic transaction on remote side"
or
    "use an atomic transaction on the remote"
or
    "request atomic transaction on remote side"
> +                       TRANSPORT_PUSH_ATOMIC),
>                 OPT_END()
>         };
>
Stefan Beller· Dec 17, 2014, 18:32 UTC · re: Stefan Beller · lore

[PATCHv3 6/6] t5543-atomic-push.sh: add basic tests for atomic pushes

This adds tests for the atomic push option. The first four tests check if the atomic option works in good conditions and the last three patches check if the atomic option prevents any change to be pushed if just one ref cannot be updated.

Signed-off-by: Stefan Beller <sbeller@google.com>
---
Notes:
    Changes in v3:
    * test_refs for checking  
    Changes v1 -> v2:
    > Please drop unused comments; they are distracting.
    
    ok
    
    > It is not wrong per-se, but haven't you already tested in
    > combination with --mirror in the previous test?
    
    I fixed the previous tests, so that there is no --mirror
    and --atomic together. There is still a first --mirror push
    for setup and a second with --atomic <branchnames> though
    
    > check_branches upstream master HEAD@{2} second HEAD~
    
    A similar function test_ref_upstream is introduced.
    
    > What's the value of this test?  Isn't it a non-fast-forward check
    > you already tested in the previous one?
    
    I messed up there. Originally I wanted to test the 2 different
    stages of rejection. A non-fast-forward check is done locally and
    we don't even try pushing. But I also want to test if we locally
    thing all is good, but the server refuses a ref to update.
    This is now done with the last test named 'atomic push obeys
    update hook preventing a branch to be pushed'. And that still fails.
    
    I'll investigate that, while still sending out the series for another
    review though.
    
    * Redone the test helper, there is test_ref_upstream now.
      This tests explicitely for SHA1 values of the ref.
      (It's needed in the last test for example. The git push fails,
      but still modifies the ref :/ )
    * checked all && chains and repaired them
    * sometimes make use of git -C <workdir>
    
    Notes v1:
    Originally Ronnie had a similar patch prepared. But as I added
    more tests and cleaned up the existing tests (e.g. use test_commit
    instead of "echo one >file && gitadd file && git commit -a -m 'one'",
    removal of dead code), the file has changed so much that I'd rather
    take ownership.
 t/t5543-atomic-push.sh | 178 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 178 insertions(+)
 create mode 100755 t/t5543-atomic-push.sh
Show changes to t/t5543-atomic-push.sh +178 −0
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
new file mode 100755
index 0000000..b81a542
--- /dev/null
+++ b/t/t5543-atomic-push.sh
@@ -0,0 +1,178 @@
+#!/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_done
-- 
2.2.0.31.gad78000.dirty

← back to recent threads