git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/7] New execute-commands hook for centralized workflow
@ 2020-03-04 11:33 Jiang Xin
  2020-03-04 11:33 ` [PATCH 1/7] receive-pack: new external execute-commands hook Jiang Xin
                   ` (22 more replies)
  0 siblings, 23 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

In my planned speech at the Git Merge 2020 conference, this series of
patches is a core part of AGit-Flow (a centralized workflow like
Gerrit). Due to a coronavirus outbreak in China, I was unable to attend
the meeting. I wrote a blog "AGit-Flow and git-repo" based on my planned
speech, see:

    https://git-repo.info/en/2020/03/agit-flow-and-git-repo/ 

Git calls an internal `execute_commands` function to handle commands
sent from the client to `git-receive-pack`.  Regardless of what
references the user pushes, git creates or updates the corresponding
references if the user has write permission.  A contributor who has
no write permission cannot push to repository directly.  So, the
contributor has to write commits to an alternate location and sends
pull request by emails or by other ways.  We call this distributed
workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user may run
the following `git push` command to push commits to a special reference
to create a code review, instead of updating a reference directly.

    git push -o reviewers=user1,user2 \
        -o oldoid=89c082363ac950d224a7259bfba3ccfbf4c560c4 \
        origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the clien-
side, such as "my/topic".

To support this kind of workflow in CGit, add a filter and a new
handler.  The filter will check the prefix of the reference name, and
if the command has a special reference name, the filter will add a
specific tag (`exec_by_hook`) to the command.  Commands with this
specific tag will be executed by a new handler (an external hook named
"execute-commands") instead of the internal `execute_commands` function.

We can use the external "execute-commands" hook to create pull requests
or send emails.  The centralized workflow is not a replacement for
the distributed workflow, but a supplement. Especially for lightweight
code contribution or for working on a project with multiple repositories.

We also implement a command line tool for this kind of workflow, see:

    https://github.com/aliyun/git-repo-go


Jiang Xin (7):
  receive-pack: new external execute-commands hook
  receive-pack: feed all commands to post-receive
  receive-pack: try `execute-commands --pre-receive`
  receive-pack: read env from execute-commands output
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.executeCommandsHookRefs
  hook: add document and example for "execute-commands" hook

 Documentation/config/receive.txt         |  11 +
 Documentation/githooks.txt               |  43 ++
 builtin/receive-pack.c                   | 228 +++++++-
 refs.c                                   |  11 +-
 refs.h                                   |   1 +
 t/t5411-execute-commands-hook.sh         | 698 +++++++++++++++++++++++
 templates/hooks--execute-commands.sample | 131 +++++
 7 files changed, 1093 insertions(+), 30 deletions(-)
 create mode 100755 t/t5411-execute-commands-hook.sh
 create mode 100755 templates/hooks--execute-commands.sample

-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH 1/7] receive-pack: new external execute-commands hook
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
@ 2020-03-04 11:33 ` Jiang Xin
  2020-03-04 11:33 ` [PATCH 2/7] receive-pack: feed all commands to post-receive Jiang Xin
                   ` (21 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references
if the user has write permission.  A contributor who has no write
permission, cannot push to the the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this distributed
workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user may run
the following `git push` command to push commits to a special reference
to create a code review, instead of updating a reference directly.

    git push -o reviewers=user1,user2 \
        -o oldoid=89c082363ac950d224a7259bfba3ccfbf4c560c4 \
        origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

In order to support this kind of workflow in CGit, add a filter and a
new handler.  The filter will check the prefix of the reference name,
and if the command has a special reference name, the filter will add a
specific tag (`exec_by_hook`) to the command.  Commands with this
specific tag will be executed by a new handler (an exeternal hook named
"execute-commands") instead of the internal `execute_commands` function.

There are two phases involved to run "execute-commands" hook:

* In order to check permissions for special `git-push` command, run a
  new "execute-commands--pre-receive" hook (or `execute-commands
  --pre-receive`, implemented in latter commit) instead of the
  "pre-receive" hook, because adding a new hook won't break the old
  implementation of the "pre-receive" hook.

* Then will call the "execute-commands" hook (without any parameter).
  This hook may call an external API to create a code review or send
  emails.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c           | 135 ++++++++--
 t/t5411-execute-commands-hook.sh | 427 +++++++++++++++++++++++++++++++
 2 files changed, 540 insertions(+), 22 deletions(-)
 create mode 100755 t/t5411-execute-commands-hook.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 411e0b4d99..24eb999ed4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -311,7 +311,8 @@ struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     exec_by_hook:1;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -668,6 +669,8 @@ static void prepare_push_cert_sha1(struct child_process *proc)
 
 struct receive_hook_feed_state {
 	struct command *cmd;
+	int exec_by_hook;
+	int hook_must_exist;
 	int skip_broken;
 	struct strbuf buf;
 	const struct string_list *push_options;
@@ -683,8 +686,13 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
 	int code;
 
 	argv[0] = find_hook(hook_name);
-	if (!argv[0])
-		return 0;
+	if (!argv[0]) {
+		if (feed_state->hook_must_exist) {
+			rp_error("cannot to find hook '%s'", hook_name);
+			return 1;
+		} else
+			return 0;
+	}
 
 	argv[1] = NULL;
 
@@ -750,9 +758,15 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
 
-	while (cmd &&
-	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
-		cmd = cmd->next;
+	while (cmd)
+		if (state->skip_broken && (cmd->error_string || cmd->did_not_exist))
+			cmd = cmd->next;
+		else if (state->exec_by_hook && !cmd->exec_by_hook)
+			cmd = cmd->next;
+		else if (!state->exec_by_hook && cmd->exec_by_hook)
+			cmd = cmd->next;
+		else
+			break;
 	if (!cmd)
 		return -1; /* EOF */
 	strbuf_reset(&state->buf);
@@ -777,6 +791,8 @@ static int run_receive_hook(struct command *commands,
 
 	strbuf_init(&state.buf, 0);
 	state.cmd = commands;
+	state.exec_by_hook = 0;
+	state.hook_must_exist = 0;
 	state.skip_broken = skip_broken;
 	if (feed_receive_hook(&state, NULL, NULL))
 		return 0;
@@ -816,14 +832,45 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
-static int is_ref_checked_out(const char *ref)
+static int run_execute_commands_pre_receive_hook(struct command *commands,
+			    const struct string_list *push_options)
 {
-	if (is_bare_repository())
+	struct receive_hook_feed_state state;
+	int status;
+
+	strbuf_init(&state.buf, 0);
+	state.cmd = commands;
+	state.exec_by_hook = 1;
+	state.hook_must_exist = 0;
+	state.skip_broken = 0;
+	if (feed_receive_hook(&state, NULL, NULL))
 		return 0;
+	state.cmd = commands;
+	state.push_options = push_options;
+	status = run_and_feed_hook("execute-commands--pre-receive",
+			feed_receive_hook, &state);
+	strbuf_release(&state.buf);
+	return status;
+}
+
+static int run_execute_commands_hook(struct command *commands,
+				     const struct string_list *push_options)
+{
+	struct receive_hook_feed_state state;
+	int status;
 
-	if (!head_name)
+	strbuf_init(&state.buf, 0);
+	state.cmd = commands;
+	state.exec_by_hook = 1;
+	state.hook_must_exist = 1;
+	state.skip_broken = 1;
+	if (feed_receive_hook(&state, NULL, NULL))
 		return 0;
-	return !strcmp(head_name, ref);
+	state.cmd = commands;
+	state.push_options = push_options;
+	status = run_and_feed_hook("execute-commands", feed_receive_hook, &state);
+	strbuf_release(&state.buf);
+	return status;
 }
 
 static char *refuse_unconfigured_deny_msg =
@@ -1373,7 +1420,7 @@ static void warn_if_skipped_connectivity_check(struct command *commands,
 	int checked_connectivity = 1;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (should_process_cmd(cmd) && si->shallow_ref[cmd->index]) {
+		if (should_process_cmd(cmd) && !cmd->exec_by_hook && si->shallow_ref[cmd->index]) {
 			error("BUG: connectivity check has not been run on ref %s",
 			      cmd->ref_name);
 			checked_connectivity = 0;
@@ -1390,7 +1437,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->exec_by_hook)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1430,7 +1477,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->exec_by_hook)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1466,6 +1513,8 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int seen_exec_by_hook = 0;
+	int seen_internal_exec = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1495,14 +1544,45 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
-	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
-		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "pre-receive hook declined";
+	/* Try to find commands that have special prefix, and will run these
+	 * commands using an external "execute-commands" hook.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->exec_by_hook = 1;
+			seen_exec_by_hook = 1;
+		} else
+			seen_internal_exec = 1;
+	}
+
+	if (seen_exec_by_hook) {
+		/* Try to find and run the `execute-commands--pre-receive` hook to check
+		 * permissions on the special commands.
+		 *
+		 * If it does not exists, try to run `execute-commands --pre-receive`.
+		 */
+		if (run_execute_commands_pre_receive_hook(commands, push_options)) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string)
+					cmd->error_string = "execute-commands hook declined";
+			}
+			return;
 		}
-		return;
 	}
 
+	if (seen_internal_exec)
+		if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string)
+					cmd->error_string = "pre-receive hook declined";
+			}
+			return;
+		}
+
 	/*
 	 * Now we'll start writing out refs, which means the objects need
 	 * to be in their final positions so that other processes can see them.
@@ -1521,10 +1601,21 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
-	if (use_atomic)
-		execute_commands_atomic(commands, si);
-	else
-		execute_commands_non_atomic(commands, si);
+	if (seen_exec_by_hook) {
+		if (run_execute_commands_hook(commands, push_options)) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->exec_by_hook || use_atomic))
+					cmd->error_string = "fail to run execute-commands hook";
+			}
+		}
+	}
+
+	if (seen_internal_exec) {
+		if (use_atomic)
+			execute_commands_atomic(commands, si);
+		else
+			execute_commands_non_atomic(commands, si);
+	}
 
 	if (shallow_update)
 		warn_if_skipped_connectivity_check(commands, si);
diff --git a/t/t5411-execute-commands-hook.sh b/t/t5411-execute-commands-hook.sh
new file mode 100755
index 0000000000..2ff0d5cbcd
--- /dev/null
+++ b/t/t5411-execute-commands-hook.sh
@@ -0,0 +1,427 @@
+#!/bin/sh
+#
+# Copyright (c) 2018-2020 Jiang Xin
+#
+
+test_description='Test execute-commands hook on special git-push refspec'
+
+. ./test-lib.sh
+
+bare=bare.git
+
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} 2>/dev/null)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+test_expect_success setup '
+	git init --bare $bare &&
+
+	# Enable push options for bare.git.
+	git -C $bare config receive.advertisePushOptions true &&
+
+	git clone --no-local $bare work &&
+	create_commits_in work A B
+'
+
+test_expect_success "setup hooks" '
+	## execute-commands--pre-receive hook
+	cat >$bare/hooks/execute-commands--pre-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: execute-commands--pre-receive\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	EOF
+
+	## execute-commands hook
+	cat >$bare/hooks/execute-commands <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: execute-commands\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	EOF
+
+	## pre-receive hook
+	cat >$bare/hooks/pre-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: pre-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	EOF
+
+	## post-receive hook
+	cat >$bare/hooks/post-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: post-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	EOF
+	chmod a+x \
+		$bare/hooks/pre-receive \
+		$bare/hooks/post-receive \
+		$bare/hooks/execute-commands \
+		$bare/hooks/execute-commands--pre-receive
+'
+
+test_expect_success "push normal branches and execute pre-receive and post-receive hooks" '
+	(
+		cd work &&
+		git update-ref HEAD $A &&
+		git push origin HEAD HEAD:maint 2>&1
+	) >out &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: 102939797ab91a4f201d131418d2c9d919dcdd2c, ref: refs/heads/master.
+	remote: >> old: 0000000000000000000000000000000000000000, new: 102939797ab91a4f201d131418d2c9d919dcdd2c, ref: refs/heads/maint.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: 102939797ab91a4f201d131418d2c9d919dcdd2c, ref: refs/heads/master.
+	remote: >> old: 0000000000000000000000000000000000000000, new: 102939797ab91a4f201d131418d2c9d919dcdd2c, ref: refs/heads/maint.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "create local topic branch" '
+	(
+		cd work &&
+		git checkout -b my/topic origin/master
+	)
+'
+
+test_expect_failure "push one special ref: refs/for/master" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		git push origin HEAD:refs/for/master/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "remove execute-commands hook" '
+	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.ok
+'
+
+test_expect_success "push branch: refs/heads/a/b/c" '
+	(
+		cd work &&
+		git update-ref HEAD $A &&
+		git push origin HEAD:a/b/c 2>&1
+	) >out &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: 102939797ab91a4f201d131418d2c9d919dcdd2c, ref: refs/heads/a/b/c.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: 102939797ab91a4f201d131418d2c9d919dcdd2c, ref: refs/heads/a/b/c.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "fail to push special ref: refs/for/master" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		test_must_fail git push origin HEAD:refs/for/master/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: error: cannot to find hook '"'"'execute-commands'"'"'
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "add back the execute-commands hook" '
+	mv $bare/hooks/execute-commands.ok $bare/hooks/execute-commands
+'
+
+test_expect_failure "push one special ref: refs/for/a/b/c" '
+	(
+		cd work &&
+		git push origin HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_failure "push two special references" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "new execute-commands hook (fail with error)" '
+	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.ok &&
+	cat >$bare/hooks/execute-commands <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: execute-commands\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	printf >&2 "fail to run execute-commands\n"
+	exit 1
+	EOF
+	chmod a+x $bare/hooks/execute-commands
+'
+
+test_expect_success "successfully push normal ref, and fail to push special reference" '
+	(
+		cd work &&
+		test_must_fail git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: execute: pre-receive hook
+	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: fail to run execute-commands
+	remote: execute: post-receive hook
+	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "restore remote master branch" '
+	(
+		cd $bare &&
+		git update-ref refs/heads/master $A $B &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-eof &&
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/a/b/c
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/maint
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/master
+	eof
+	test_cmp expect actual
+'
+
+test_expect_success "all mixed refs are failed to push in atomic mode" '
+	(
+		cd work &&
+		test_must_fail git push --atomic origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: execute: pre-receive hook
+	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: fail to run execute-commands
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "restore execute-commands hook" '
+	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.fail &&
+	mv $bare/hooks/execute-commands.ok $bare/hooks/execute-commands
+'
+
+test_expect_failure "push mixed references successfully" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: execute: pre-receive hook
+	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "restore remote master branch" '
+	(
+		cd $bare &&
+		git update-ref refs/heads/master $A $B &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/a/b/c
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/maint
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "new execute-commands--pre-receive hook (declined version)" '
+	mv $bare/hooks/execute-commands--pre-receive $bare/hooks/execute-commands--pre-receive.ok &&
+	cat >$bare/hooks/execute-commands--pre-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: execute-commands--pre-receive\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	printf >&2 ">> ERROR: declined in execute-commands--pre-receive\n"
+	exit 1
+	EOF
+	chmod a+x $bare/hooks/execute-commands--pre-receive
+'
+
+test_expect_success "cannot push two special references (declined)" '
+	(
+		cd work &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/my/topic \
+			HEAD:refs/for/maint/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> ERROR: declined in execute-commands--pre-receive
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cannot push mixed references (declined)" '
+	(
+		cd work &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: >> ERROR: declined in execute-commands--pre-receive
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "new pre-receive hook (declined version)" '
+	mv $bare/hooks/execute-commands--pre-receive $bare/hooks/execute-commands--pre-receive.fail &&
+	mv $bare/hooks/execute-commands--pre-receive.ok $bare/hooks/execute-commands--pre-receive &&
+	mv $bare/hooks/pre-receive $bare/hooks/pre-receive.ok &&
+	cat >$bare/hooks/pre-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: pre-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	printf >&2 ">> ERROR: declined in pre-receive hook\n"
+	exit 1
+	EOF
+	chmod a+x $bare/hooks/pre-receive
+'
+
+test_expect_success "cannot push mixed references (declined)" '
+	(
+		cd work &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands--pre-receive
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: execute: pre-receive hook
+	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
+	remote: >> ERROR: declined in pre-receive hook
+	EOF
+	test_cmp expect actual
+'
+
+test_done
-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 2/7] receive-pack: feed all commands to post-receive
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
  2020-03-04 11:33 ` [PATCH 1/7] receive-pack: new external execute-commands hook Jiang Xin
@ 2020-03-04 11:33 ` Jiang Xin
  2020-03-04 11:33 ` [PATCH 3/7] receive-pack: try `execute-commands --pre-receive` Jiang Xin
                   ` (20 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

Feed all commands to "post-receive" hook, not only normal commands, but
also commands with "exec_by_hook" flag.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c           | 11 +++++++++--
 t/t5411-execute-commands-hook.sh |  8 ++++----
 2 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 24eb999ed4..c97abfbcd3 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -669,6 +669,7 @@ static void prepare_push_cert_sha1(struct child_process *proc)
 
 struct receive_hook_feed_state {
 	struct command *cmd;
+	int exec_all;
 	int exec_by_hook;
 	int hook_must_exist;
 	int skip_broken;
@@ -761,6 +762,8 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	while (cmd)
 		if (state->skip_broken && (cmd->error_string || cmd->did_not_exist))
 			cmd = cmd->next;
+		else if (state->exec_all)
+		       break;
 		else if (state->exec_by_hook && !cmd->exec_by_hook)
 			cmd = cmd->next;
 		else if (!state->exec_by_hook && cmd->exec_by_hook)
@@ -784,6 +787,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 static int run_receive_hook(struct command *commands,
 			    const char *hook_name,
 			    int skip_broken,
+			    int exec_all,
 			    const struct string_list *push_options)
 {
 	struct receive_hook_feed_state state;
@@ -791,6 +795,7 @@ static int run_receive_hook(struct command *commands,
 
 	strbuf_init(&state.buf, 0);
 	state.cmd = commands;
+	state.exec_all = exec_all;
 	state.exec_by_hook = 0;
 	state.hook_must_exist = 0;
 	state.skip_broken = skip_broken;
@@ -840,6 +845,7 @@ static int run_execute_commands_pre_receive_hook(struct command *commands,
 
 	strbuf_init(&state.buf, 0);
 	state.cmd = commands;
+	state.exec_all = 0;
 	state.exec_by_hook = 1;
 	state.hook_must_exist = 0;
 	state.skip_broken = 0;
@@ -861,6 +867,7 @@ static int run_execute_commands_hook(struct command *commands,
 
 	strbuf_init(&state.buf, 0);
 	state.cmd = commands;
+	state.exec_all = 0;
 	state.exec_by_hook = 1;
 	state.hook_must_exist = 1;
 	state.skip_broken = 1;
@@ -1575,7 +1582,7 @@ static void execute_commands(struct command *commands,
 	}
 
 	if (seen_internal_exec)
-		if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
+		if (run_receive_hook(commands, "pre-receive", 0, 0, push_options)) {
 			for (cmd = commands; cmd; cmd = cmd->next) {
 				if (!cmd->error_string)
 					cmd->error_string = "pre-receive hook declined";
@@ -2114,7 +2121,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 			unlink_or_warn(pack_lockfile);
 		if (report_status)
 			report(commands, unpack_status);
-		run_receive_hook(commands, "post-receive", 1,
+		run_receive_hook(commands, "post-receive", 1, 1,
 				 &push_options);
 		run_update_post_hook(commands);
 		string_list_clear(&push_options, 0);
diff --git a/t/t5411-execute-commands-hook.sh b/t/t5411-execute-commands-hook.sh
index 2ff0d5cbcd..b6444ca047 100755
--- a/t/t5411-execute-commands-hook.sh
+++ b/t/t5411-execute-commands-hook.sh
@@ -125,7 +125,7 @@ test_expect_success "create local topic branch" '
 	)
 '
 
-test_expect_failure "push one special ref: refs/for/master" '
+test_expect_success "push one special ref: refs/for/master" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -182,7 +182,7 @@ test_expect_success "add back the execute-commands hook" '
 	mv $bare/hooks/execute-commands.ok $bare/hooks/execute-commands
 '
 
-test_expect_failure "push one special ref: refs/for/a/b/c" '
+test_expect_success "push one special ref: refs/for/a/b/c" '
 	(
 		cd work &&
 		git push origin HEAD:refs/for/a/b/c/my/topic
@@ -199,7 +199,7 @@ test_expect_failure "push one special ref: refs/for/a/b/c" '
 	test_cmp expect actual
 '
 
-test_expect_failure "push two special references" '
+test_expect_success "push two special references" '
 	(
 		cd work &&
 		git push origin \
@@ -300,7 +300,7 @@ test_expect_success "restore execute-commands hook" '
 	mv $bare/hooks/execute-commands.ok $bare/hooks/execute-commands
 '
 
-test_expect_failure "push mixed references successfully" '
+test_expect_success "push mixed references successfully" '
 	(
 		cd work &&
 		git push origin \
-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 3/7] receive-pack: try `execute-commands --pre-receive`
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
  2020-03-04 11:33 ` [PATCH 1/7] receive-pack: new external execute-commands hook Jiang Xin
  2020-03-04 11:33 ` [PATCH 2/7] receive-pack: feed all commands to post-receive Jiang Xin
@ 2020-03-04 11:33 ` Jiang Xin
  2020-03-04 11:33 ` [PATCH 4/7] receive-pack: read env from execute-commands output Jiang Xin
                   ` (19 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

First we try to find the hook "execute-command--pre-receive" to check
permissions for special commands.  If the hook does not exist, will try
to find the hook "execute-commands" and run command `execute-commands
--pre-receive` instead.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c           | 30 ++++++++++----
 t/t5411-execute-commands-hook.sh | 68 +++++++++++++++++++++++---------
 2 files changed, 71 insertions(+), 27 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index c97abfbcd3..241b1d4cfc 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -683,20 +683,34 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
 {
 	struct child_process proc = CHILD_PROCESS_INIT;
 	struct async muxer;
-	const char *argv[2];
+	const char *argv[3];
 	int code;
 
 	argv[0] = find_hook(hook_name);
 	if (!argv[0]) {
-		if (feed_state->hook_must_exist) {
-			rp_error("cannot to find hook '%s'", hook_name);
-			return 1;
-		} else
-			return 0;
+		char *hook_helper;
+		char *opt;
+
+		opt = strstr(hook_name, "--");
+		if (opt) {
+			hook_helper = xstrdup(hook_name);
+			hook_helper[opt - hook_name] = '\0';
+			argv[0] = find_hook(hook_helper);
+			free(hook_helper);
+		}
+		if (!argv[0]) {
+			if (feed_state->hook_must_exist) {
+				rp_error("cannot to find hook '%s'", hook_name);
+				return 1;
+			} else
+				return 0;
+		}
+		argv[1] = opt;
+		argv[2] = NULL;
+	} else {
+		argv[1] = NULL;
 	}
 
-	argv[1] = NULL;
-
 	proc.argv = argv;
 	proc.in = -1;
 	proc.stdout_to_stderr = 1;
diff --git a/t/t5411-execute-commands-hook.sh b/t/t5411-execute-commands-hook.sh
index b6444ca047..0bf14e702d 100755
--- a/t/t5411-execute-commands-hook.sh
+++ b/t/t5411-execute-commands-hook.sh
@@ -64,6 +64,11 @@ test_expect_success "setup hooks" '
 
 	printf >&2 "execute: execute-commands\n"
 
+	if test \$# -gt 0 && test "\$1" = "--pre-receive"
+	then
+		printf >&2 ">> pre-receive mode\n"
+	fi
+
 	while read old new ref
 	do
 		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
@@ -183,13 +188,15 @@ test_expect_success "add back the execute-commands hook" '
 '
 
 test_expect_success "push one special ref: refs/for/a/b/c" '
+	mv $bare/hooks/execute-commands--pre-receive $bare/hooks/execute-commands--pre-receive.ok &&
 	(
 		cd work &&
 		git push origin HEAD:refs/for/a/b/c/my/topic
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
 	remote: execute: execute-commands
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
@@ -208,7 +215,8 @@ test_expect_success "push two special references" '
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
 	remote: execute: execute-commands
@@ -228,13 +236,21 @@ test_expect_success "new execute-commands hook (fail with error)" '
 
 	printf >&2 "execute: execute-commands\n"
 
+	if test \$# -gt 0 && test "\$1" = "--pre-receive"
+	then
+		printf >&2 ">> pre-receive mode\n"
+	fi
+
 	while read old new ref
 	do
 		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
 	done
 
-	printf >&2 "fail to run execute-commands\n"
-	exit 1
+	if test \$# -eq 0
+	then
+		printf >&2 "fail to run execute-commands\n"
+		exit 1
+	fi
 	EOF
 	chmod a+x $bare/hooks/execute-commands
 '
@@ -248,7 +264,8 @@ test_expect_success "successfully push normal ref, and fail to push special refe
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
 	remote: execute: pre-receive hook
 	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
@@ -284,7 +301,8 @@ test_expect_success "all mixed refs are failed to push in atomic mode" '
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
 	remote: execute: pre-receive hook
 	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
@@ -309,7 +327,8 @@ test_expect_success "push mixed references successfully" '
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
 	remote: execute: pre-receive hook
 	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
@@ -336,22 +355,30 @@ test_expect_success "restore remote master branch" '
 	test_cmp expect actual
 '
 
-test_expect_success "new execute-commands--pre-receive hook (declined version)" '
-	mv $bare/hooks/execute-commands--pre-receive $bare/hooks/execute-commands--pre-receive.ok &&
-	cat >$bare/hooks/execute-commands--pre-receive <<-EOF &&
+test_expect_success "new execute-commands hook (pre-receive declined)" '
+	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.ok &&
+	cat >$bare/hooks/execute-commands <<-EOF &&
 	#!/bin/sh
 
-	printf >&2 "execute: execute-commands--pre-receive\n"
+	printf >&2 "execute: execute-commands\n"
+
+	if test \$# -gt 0 && test "\$1" = "--pre-receive"
+	then
+		printf >&2 ">> pre-receive mode\n"
+	fi
 
 	while read old new ref
 	do
 		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
 	done
 
-	printf >&2 ">> ERROR: declined in execute-commands--pre-receive\n"
-	exit 1
+	if test \$# -gt 0 && test "\$1" = "--pre-receive"
+	then
+		printf >&2 ">> ERROR: declined in execute-commands--pre-receive\n"
+		exit 1
+	fi
 	EOF
-	chmod a+x $bare/hooks/execute-commands--pre-receive
+	chmod a+x $bare/hooks/execute-commands
 '
 
 test_expect_success "cannot push two special references (declined)" '
@@ -363,7 +390,8 @@ test_expect_success "cannot push two special references (declined)" '
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
 	remote: >> ERROR: declined in execute-commands--pre-receive
@@ -380,7 +408,8 @@ test_expect_success "cannot push mixed references (declined)" '
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
 	remote: >> ERROR: declined in execute-commands--pre-receive
 	EOF
@@ -388,8 +417,8 @@ test_expect_success "cannot push mixed references (declined)" '
 '
 
 test_expect_success "new pre-receive hook (declined version)" '
-	mv $bare/hooks/execute-commands--pre-receive $bare/hooks/execute-commands--pre-receive.fail &&
-	mv $bare/hooks/execute-commands--pre-receive.ok $bare/hooks/execute-commands--pre-receive &&
+	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.fail &&
+	mv $bare/hooks/execute-commands.ok $bare/hooks/execute-commands &&
 	mv $bare/hooks/pre-receive $bare/hooks/pre-receive.ok &&
 	cat >$bare/hooks/pre-receive <<-EOF &&
 	#!/bin/sh
@@ -415,7 +444,8 @@ test_expect_success "cannot push mixed references (declined)" '
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
-	remote: execute: execute-commands--pre-receive
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
 	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
 	remote: execute: pre-receive hook
 	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 4/7] receive-pack: read env from execute-commands output
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (2 preceding siblings ...)
  2020-03-04 11:33 ` [PATCH 3/7] receive-pack: try `execute-commands --pre-receive` Jiang Xin
@ 2020-03-04 11:33 ` Jiang Xin
  2020-03-04 11:33 ` [PATCH 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                   ` (18 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

The “post-receive” hook may need the pull request ID generated by the
“execute-commands” hook.  The results can be passed between hooks by
environment variables.

Each line of the message received from the standard output of the
“execute-commands” in the key=value format is parsed as environment and
these variables will be sent to environment of the “post-receive” hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c           | 42 +++++++++++++++--
 t/t5411-execute-commands-hook.sh | 79 ++++++++++++++++++++++++++++++++
 2 files changed, 117 insertions(+), 4 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 241b1d4cfc..fd2f3ba80a 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -85,6 +85,7 @@ static const char *nonce_status;
 static long nonce_stamp_slop;
 static timestamp_t nonce_stamp_slop_limit;
 static struct ref_transaction *transaction;
+struct argv_array post_receive_env_array;
 
 static enum {
 	KEEPALIVE_NEVER = 0,
@@ -678,7 +679,9 @@ struct receive_hook_feed_state {
 };
 
 typedef int (*feed_fn)(void *, const char **, size_t *);
+typedef void (*stdout_handler_fn)(int out);
 static int run_and_feed_hook(const char *hook_name, feed_fn feed,
+			     stdout_handler_fn stdout_handler,
 			     struct receive_hook_feed_state *feed_state)
 {
 	struct child_process proc = CHILD_PROCESS_INIT;
@@ -713,9 +716,15 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
 
 	proc.argv = argv;
 	proc.in = -1;
-	proc.stdout_to_stderr = 1;
+	if (stdout_handler)
+		proc.out = -1;
+	else
+		proc.stdout_to_stderr = 1;
 	proc.trace2_hook_name = hook_name;
 
+	if (!strcmp(hook_name, "post-receive") && post_receive_env_array.argc > 0)
+		argv_array_pushv(&proc.env_array, post_receive_env_array.argv);
+
 	if (feed_state->push_options) {
 		int i;
 		for (i = 0; i < feed_state->push_options->nr; i++)
@@ -760,6 +769,10 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
 			break;
 	}
 	close(proc.in);
+
+	if (stdout_handler)
+		stdout_handler(proc.out);
+
 	if (use_sideband)
 		finish_async(&muxer);
 
@@ -817,7 +830,7 @@ static int run_receive_hook(struct command *commands,
 		return 0;
 	state.cmd = commands;
 	state.push_options = push_options;
-	status = run_and_feed_hook(hook_name, feed_receive_hook, &state);
+	status = run_and_feed_hook(hook_name, feed_receive_hook, NULL, &state);
 	strbuf_release(&state.buf);
 	return status;
 }
@@ -868,11 +881,29 @@ static int run_execute_commands_pre_receive_hook(struct command *commands,
 	state.cmd = commands;
 	state.push_options = push_options;
 	status = run_and_feed_hook("execute-commands--pre-receive",
-			feed_receive_hook, &state);
+			feed_receive_hook, NULL, &state);
 	strbuf_release(&state.buf);
 	return status;
 }
 
+
+static void prepare_post_receive_env(int in)
+{
+	struct strbuf stdout_buf = STRBUF_INIT;
+
+	while (strbuf_getwholeline_fd(&stdout_buf, in, '\n') != EOF) {
+		char *p = stdout_buf.buf + stdout_buf.len -1;
+		if (*p =='\n')
+			*p = '\0';
+		p = strchr(stdout_buf.buf, '=');
+		if (p == NULL)
+			continue;
+		argv_array_push(&post_receive_env_array, stdout_buf.buf);
+		strbuf_reset(&stdout_buf);
+	}
+	strbuf_release(&stdout_buf);
+}
+
 static int run_execute_commands_hook(struct command *commands,
 				     const struct string_list *push_options)
 {
@@ -889,7 +920,8 @@ static int run_execute_commands_hook(struct command *commands,
 		return 0;
 	state.cmd = commands;
 	state.push_options = push_options;
-	status = run_and_feed_hook("execute-commands", feed_receive_hook, &state);
+	status = run_and_feed_hook("execute-commands",
+			feed_receive_hook, prepare_post_receive_env, &state);
 	strbuf_release(&state.buf);
 	return status;
 }
@@ -2052,6 +2084,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	argv_array_init(&post_receive_env_array);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
diff --git a/t/t5411-execute-commands-hook.sh b/t/t5411-execute-commands-hook.sh
index 0bf14e702d..1907d0619d 100755
--- a/t/t5411-execute-commands-hook.sh
+++ b/t/t5411-execute-commands-hook.sh
@@ -454,4 +454,83 @@ test_expect_success "cannot push mixed references (declined)" '
 	test_cmp expect actual
 '
 
+test_expect_success "new execute-commands and post-receive hooks (environments in output)" '
+	## execute-commands hook
+	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.ok &&
+	cat >$bare/hooks/execute-commands <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: execute-commands\n"
+
+	if test \$# -gt 0 && test "\$1" = "--pre-receive"
+	then
+		printf >&2 ">> pre-receive mode\n"
+	else
+		printf "GIT_VAR1=var1\n"
+		printf "GIT_VAR2=var2\n"
+		printf "AGIT_VAR1=foo\n"
+		printf "AGIT_VAR2=bar\n"
+	fi
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	for k in GIT_VAR1 GIT_VAR2 AGIT_VAR1 AGIT_VAR2
+	do
+		if test -n "\$(eval echo \\"\\\$\$k\")"
+		then
+			printf >&2 ">> has env: \$k=\$(eval echo \\"\\\$\$k\").\n"
+		fi
+	done
+	EOF
+	chmod a+x $bare/hooks/execute-commands &&
+
+	## post-receive hook
+	mv $bare/hooks/post-receive $bare/hooks/post-receive.ok &&
+	cat >$bare/hooks/post-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: post-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	for k in GIT_VAR1 GIT_VAR2 AGIT_VAR1 AGIT_VAR2
+	do
+		if test -n "\$(eval echo \\"\\\$\$k\")"
+		then
+			printf >&2 ">> has env: \$k=\$(eval echo \\"\\\$\$k\").\n"
+		fi
+	done
+	EOF
+	chmod a+x $bare/hooks/post-receive
+'
+
+test_expect_success "push and show environments" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/master/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: >> has env: GIT_VAR1=var1.
+	remote: >> has env: GIT_VAR2=var2.
+	remote: >> has env: AGIT_VAR1=foo.
+	remote: >> has env: AGIT_VAR2=bar.
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 5/7] refs.c: refactor to reuse ref_is_hidden()
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (3 preceding siblings ...)
  2020-03-04 11:33 ` [PATCH 4/7] receive-pack: read env from execute-commands output Jiang Xin
@ 2020-03-04 11:33 ` Jiang Xin
  2020-03-04 11:33 ` [PATCH 6/7] receive-pack: new config receive.executeCommandsHookRefs Jiang Xin
                   ` (17 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Test
case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 6/7] receive-pack: new config receive.executeCommandsHookRefs
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (4 preceding siblings ...)
  2020-03-04 11:33 ` [PATCH 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-03-04 11:33 ` Jiang Xin
  2020-03-04 11:33 ` [PATCH 7/7] hook: add document and example for "execute-commands" hook Jiang Xin
                   ` (16 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

Add a new multi-valued config variable "receive.executeCommandsHookRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.executeCommandsHookRefs refs/for/
    git config --system --add receive.executeCommandsHookRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "execute-commands"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.  Use more than one definition to specify multiple prefix strings.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt | 11 +++++
 builtin/receive-pack.c           | 48 ++++++++++++++++-----
 t/t5411-execute-commands-hook.sh | 72 ++++++++++++++++++++++++++++++--
 3 files changed, 117 insertions(+), 14 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..a8e2125e62 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -108,6 +108,17 @@ receive.denyNonFastForwards::
 	even if that push is forced. This configuration variable is
 	set when initializing a shared repository.
 
+receive.executeCommandsHookRefs::
+	If the specific prefix matches the reference name of the command
+	which is sent by the client to `receive-pack`, the command will be
+	executed by an external hook (named "execute-commands"), instead of
+	the internal `execute_commands` function. For example, if it is
+	set to "refs/for/", pushing to reference such as "refs/for/master"
+	will not create or update a reference named "refs/for/master", but
+	may create or update a pull request directly by running the external
+	hook. Use more than one definition to specify multiple prefix
+	strings.
+
 receive.hideRefs::
 	This variable is the same as `transfer.hideRefs`, but applies
 	only to `receive-pack` (and so affects pushes, but not fetches).
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index fd2f3ba80a..f78adeffd4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -75,6 +75,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list execute_commands_hook_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.executecommandshookrefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&execute_commands_hook_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1600,17 +1615,29 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix, and will run these
 	 * commands using an external "execute-commands" hook.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (execute_commands_hook_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->exec_by_hook = 1;
-			seen_exec_by_hook = 1;
-		} else
-			seen_internal_exec = 1;
-	}
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&execute_commands_hook_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->exec_by_hook = 1;
+				seen_exec_by_hook = 1;
+			} else
+				seen_internal_exec = 1;
+		}
+
+		strbuf_release(&refname_full);
+	} else
+		seen_internal_exec = 1;
 
 	if (seen_exec_by_hook) {
 		/* Try to find and run the `execute-commands--pre-receive` hook to check
@@ -2085,6 +2112,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	};
 
 	argv_array_init(&post_receive_env_array);
+	string_list_init(&execute_commands_hook_refs, 0);
 
 	packet_trace_identity("receive-pack");
 
diff --git a/t/t5411-execute-commands-hook.sh b/t/t5411-execute-commands-hook.sh
index 1907d0619d..c8ee699773 100755
--- a/t/t5411-execute-commands-hook.sh
+++ b/t/t5411-execute-commands-hook.sh
@@ -41,6 +41,9 @@ test_expect_success setup '
 	# Enable push options for bare.git.
 	git -C $bare config receive.advertisePushOptions true &&
 
+	# Register ref prefix for execute-commands hook.
+	git -C $bare config --add receive.executeCommandsHookRefs refs/for/ &&
+
 	git clone --no-local $bare work &&
 	create_commits_in work A B
 '
@@ -229,6 +232,67 @@ test_expect_success "push two special references" '
 	test_cmp expect actual
 '
 
+test_expect_success "push two special references, but one is not registered" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/drafts/maint/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: execute: pre-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "restore bare.git, and register new ref prefix" '
+	(
+		cd $bare &&
+		git config --add receive.executeCommandsHookRefs refs/drafts/ &&
+		git update-ref -d refs/drafts/maint/my/topic &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/a/b/c
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/maint
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "push to two special references (all registered)" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/master/my/topic \
+			HEAD:refs/drafts/maint/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: execute-commands
+	remote: >> pre-receive mode
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
+	remote: execute: execute-commands
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/master/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
 test_expect_success "new execute-commands hook (fail with error)" '
 	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.ok &&
 	cat >$bare/hooks/execute-commands <<-EOF &&
@@ -322,21 +386,21 @@ test_expect_success "push mixed references successfully" '
 	(
 		cd work &&
 		git push origin \
-			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/drafts/maint/my/topic \
 			HEAD:refs/heads/master
 	) >out 2>&1 &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
 	remote: execute: execute-commands
 	remote: >> pre-receive mode
-	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
 	remote: execute: pre-receive hook
 	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
 	remote: execute: execute-commands
-	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
 	remote: execute: post-receive hook
 	remote: >> old: 102939797ab91a4f201d131418d2c9d919dcdd2c, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/heads/master.
-	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/maint/my/topic.
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/drafts/maint/my/topic.
 	EOF
 	test_cmp expect actual
 '
-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 7/7] hook: add document and example for "execute-commands" hook
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (5 preceding siblings ...)
  2020-03-04 11:33 ` [PATCH 6/7] receive-pack: new config receive.executeCommandsHookRefs Jiang Xin
@ 2020-03-04 11:33 ` Jiang Xin
  2020-03-04 20:39 ` [PATCH 0/7] New execute-commands hook for centralized workflow Junio C Hamano
                   ` (15 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-04 11:33 UTC (permalink / raw)
  To: Git List; +Cc: Jiang Xin, Junio C Hamano

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt               |  43 ++++++++
 t/t5411-execute-commands-hook.sh         |  98 +++++++++++++++++
 templates/hooks--execute-commands.sample | 131 +++++++++++++++++++++++
 3 files changed, 272 insertions(+)
 create mode 100755 templates/hooks--execute-commands.sample

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..6c21ab6db2 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -58,6 +58,49 @@ the message file.
 The default 'applypatch-msg' hook, when enabled, runs the
 'commit-msg' hook, if the latter is enabled.
 
+execute-commands--pre-receive
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  Just before starting to execute the
+external 'execute-commands' hook to make changes to the repository,
+the 'execute-commands--pre-receive' hook is invoked.  This hook has
+the same functionality as 'pre-receive' hook, while it only acts on
+special commands which 'git-push' sends to 'git-receive-pack'.
+The refnames of these spacial commands have special prefix (such as
+"refs/for/") which matches what the `receive.executeCommandsHookRefs`
+configuration variable defines.
+
+If this hook does not exist, will try to find the 'execute-commands'
+hook, and run `execute-command --pre-receive` command to do some
+checks.  Its exit status determines the success or failure of the
+update.  If the hook exits with non-zero status, won't execute any of
+the commands, no metter calling the internal `execute_commands`
+function or calling the external "execute-commands" hook.
+
+This hook executes once for the receive operation, and gets the
+information from its standard input.
+See <<pre-receive,'pre-receive'>> for details.
+
+execute-commands
+~~~~~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  According to refnames of the commands which
+`git push` sends to 'git-receive-pack', the commands will be devided
+into two groups by matching what the `receive.executeCommandsHookRefs`
+configuration variable defines.  One group of the commands will execute
+the internal `execute_commands` function to update the corresponding
+refnames, and the other group of commands which have matching refnames
+will execute this external 'execute-commands' hook to create pull
+requests, etc.
+
+Its exit status only determines the success or failure of the group of
+commands with special refnames, unless atomic push is in use.
+
+This hook executes once if there are any special commands with special
+refnames.  There is no argument taken by this hook, and the push options
+(if any) and command(s) will be fed to this book by its standard input.
+See the <<pre-receive,'pre-receive'>> hook for the format of the input.
+
 pre-applypatch
 ~~~~~~~~~~~~~~
 
diff --git a/t/t5411-execute-commands-hook.sh b/t/t5411-execute-commands-hook.sh
index c8ee699773..6dd1560f9d 100755
--- a/t/t5411-execute-commands-hook.sh
+++ b/t/t5411-execute-commands-hook.sh
@@ -597,4 +597,102 @@ test_expect_success "push and show environments" '
 	test_cmp expect actual
 '
 
+test_expect_success "execute-commands.sample: new execute-commands hook from templates/execute-commands.sample" '
+	mv $bare/hooks/pre-receive $bare/hooks/pre-receive.fail &&
+	mv $bare/hooks/pre-receive.ok $bare/hooks/pre-receive &&
+	mv $bare/hooks/post-receive $bare/hooks/post-receive.env &&
+	mv $bare/hooks/post-receive.ok $bare/hooks/post-receive &&
+	mv $bare/hooks/execute-commands $bare/hooks/execute-commands.env &&
+	cp ../../templates/hooks--execute-commands.sample $bare/hooks/execute-commands &&
+	chmod a+x $bare/hooks/execute-commands
+'
+
+test_expect_success "execute-commands.sample: show push result" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: 102939797ab91a4f201d131418d2c9d919dcdd2c
+	remote: [execute-commands] *******************************************************
+	remote: [execute-commands] * Pull request #12345678901 created/updated           *
+	remote: [execute-commands] * URL: https://... ...                                *
+	remote: [execute-commands] *******************************************************
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "execute-commands.sample: show debug info" '
+	(
+		cd work &&
+		git push -o debug=1 -o reviewers=user1,user2 \
+			origin \
+			HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: [DEBUG] [execute-commands] push-option: AGIT_DEBUG=1
+	remote: [DEBUG] [execute-commands] push-option: AGIT_REVIEWERS=user1,user2
+	remote: [DEBUG] [execute-commands] command from stdin: 0000000000000000000000000000000000000000 ce858e653cdbf70f9955a39d73a44219e4b92e9e refs/for/a/b/c/my/topic
+	remote: 102939797ab91a4f201d131418d2c9d919dcdd2c
+	remote: [DEBUG] [execute-commands: pre-receive] check permissions...
+	remote: [DEBUG] [execute-commands] push-option: AGIT_DEBUG=1
+	remote: [DEBUG] [execute-commands] push-option: AGIT_REVIEWERS=user1,user2
+	remote: [DEBUG] [execute-commands] command from stdin: 0000000000000000000000000000000000000000 ce858e653cdbf70f9955a39d73a44219e4b92e9e refs/for/a/b/c/my/topic
+	remote: [DEBUG] [execute-commands] call API (AGIT_PR_TARGET=a/b/c, AGIT_PR_TOPIC=)...
+	remote: [DEBUG] [execute-commands] parse API result, and get AGIT_PR_ID, etc.
+	remote: [execute-commands] *******************************************************
+	remote: [execute-commands] * Pull request #12345678901 created/updated           *
+	remote: [execute-commands] * URL: https://... ...                                *
+	remote: [execute-commands] *******************************************************
+	remote: [DEBUG] [execute-commands] output kv pairs to stdout for git to parse.
+	remote: execute: post-receive hook
+	remote: >> old: 0000000000000000000000000000000000000000, new: ce858e653cdbf70f9955a39d73a44219e4b92e9e, ref: refs/for/a/b/c/my/topic.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "execute-commands.sample: fail to push refs/for/maint" '
+	(
+		cd work &&
+		test_must_fail git push -o reviewers=user1,user2 \
+			origin \
+			HEAD:refs/for/maint/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: 102939797ab91a4f201d131418d2c9d919dcdd2c
+	remote: [execute-commands: pre-receive] send pull request to maint branch is not allowed
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "execute-commands.sample: fail to push non-exist branch" '
+	(
+		cd work &&
+		test_must_fail git push -o reviewers=user1,user2 \
+			origin \
+			HEAD:refs/for/a/b/x/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: [execute-commands] cannot find target branch from ref: refs/for/a/b/x/my/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "show refs of the repository using git-show-ref" '
+	git -C $bare show-ref >actual &&
+	cat >expect <<-EOF &&
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/a/b/c
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/maint
+	102939797ab91a4f201d131418d2c9d919dcdd2c refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
 test_done
diff --git a/templates/hooks--execute-commands.sample b/templates/hooks--execute-commands.sample
new file mode 100755
index 0000000000..d061984bca
--- /dev/null
+++ b/templates/hooks--execute-commands.sample
@@ -0,0 +1,131 @@
+#!/bin/sh
+#
+# This is an  example hook script, DO NOT use it on production service.
+
+debug() {
+	case "$AGIT_DEBUG" in
+	"yes" | "true" | "1")
+		;;
+	*)
+		return
+	esac
+
+	echo >&2 "[DEBUG] $@"
+}
+
+# Parse push options
+if test -n "$GIT_PUSH_OPTION_COUNT"
+then
+	i=0
+	while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
+	do
+		eval "value=\$GIT_PUSH_OPTION_$i"
+		i=$((i + 1))
+
+		k=$(echo ${value%=*} | tr [a-z] [A-Z])
+		v=${value#*=}
+		if test -n "$v" && test -n "$k"
+		then
+			k="AGIT_$k"
+		else
+			continue
+		fi
+		eval "$k=$v"
+		debug "[execute-commands] push-option: $k=$v"
+	done
+fi
+
+# Read push commands.
+count=0
+while read old new refname
+do
+	debug "[execute-commands] command from stdin: $old $new $refname"
+	count=$(( count + 1 ))
+	# Only one special refname is allowed for each push
+	if test $count -gt 1
+	then
+		echo >&2 "[execute-commands]: cannot handle more than one push commands"
+		exit 1
+	fi
+
+	# Parse refname, and set envrionment
+	remains=
+	if test "${refname#refs/for/}" != "$refname"
+	then
+		AGIT_PR_IS_DRAFT=false
+		remains=${refname#refs/for/}
+	elif test "${refname#refs/drafts/}" != "$refname"
+	then
+		AGIT_PR_IS_DRAFT=true
+		remains=${refname#refs/drafts/}
+	else
+		echo >&2 "[execute-commands] unknown refname: $refname"
+		exit 1
+	fi
+
+	ref=
+	found_ref=
+	for i in $(echo $remains | tr "/" "\n")
+	do
+		if test -z "$ref"
+		then
+			ref=$i
+		else
+			ref=$ref/$i
+		fi
+		if git rev-parse --verify $ref -- 2>/dev/null
+		then
+			found_ref=yes
+			break
+		fi
+	done
+	if test -z "$found_ref"
+	then
+		echo >&2 "[execute-commands] cannot find target branch from ref: $refname"
+		exit 1
+	fi
+	AGIT_PR_TARGET=$ref
+	AGIT_PR_SOURCE=${remains#$ref/}
+done
+
+if test -z "$AGIT_PR_TARGET"
+then
+	echo >&2 "[execute-commands] fail to parse refname, no target found"
+	exit 1
+fi
+
+# pre-receive mode, used to check permissions.
+if test "$1" = "--pre-receive"
+then
+	debug "[execute-commands: pre-receive] check permissions..."
+	if test "$AGIT_PR_TARGET" = "maint"
+	then
+		echo >&2 "[execute-commands: pre-receive] send pull request to maint branch is not allowed"
+		exit 1
+	fi
+	exit 0
+fi
+
+# Call API to generate code review.
+debug "[execute-commands] call API (AGIT_PR_TARGET=$AGIT_PR_TARGET, AGIT_PR_TOPIC=$AGIT_PR_TOPIC)..."
+
+# Parse result of API.
+debug "[execute-commands] parse API result, and get AGIT_PR_ID, etc."
+AGIT_PR_ID="12345678901"
+AGIT_PR_LOCAL_ID="23"
+
+# Show message.
+if test -n "$AGIT_PR_ID"
+then
+	echo >&2 "[execute-commands] *******************************************************"
+	echo >&2 "[execute-commands] * Pull request #$AGIT_PR_ID created/updated           *"
+	echo >&2 "[execute-commands] * URL: https://... ...                                *"
+	echo >&2 "[execute-commands] *******************************************************"
+fi
+
+# Show envs to stdout, and will be exported as envs for "post-receive" hook.
+debug "[execute-commands] output kv pairs to stdout for git to parse."
+echo "AGIT_PR_ID=$AGIT_PR_ID"
+echo "AGIT_PR_LOCAL_ID=$AGIT_PR_LOCAL_ID"
+
+exit 0
-- 
2.25.1.362.g51ebf55b93


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH 0/7] New execute-commands hook for centralized workflow
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (6 preceding siblings ...)
  2020-03-04 11:33 ` [PATCH 7/7] hook: add document and example for "execute-commands" hook Jiang Xin
@ 2020-03-04 20:39 ` Junio C Hamano
  2020-03-05 16:51   ` Jiang Xin
  2020-03-30 16:57 ` [PATCH v5 0/6] New proc-receive hook for centralized workflow Jiang Xin
                   ` (14 subsequent siblings)
  22 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-03-04 20:39 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> It would be more convenient to work in a centralized workflow like what
> Gerrit provided for some cases.  For example, a read-only user may run
> the following `git push` command to push commits to a special reference
> to create a code review, instead of updating a reference directly.
>
>     git push -o reviewers=user1,user2 \
>         -o oldoid=89c082363ac950d224a7259bfba3ccfbf4c560c4 \
>         origin \
>         HEAD:refs/for/<branch-name>/<session>
>
> The `<branch-name>` in the above example can be as simple as "master",
> or a more complicated branch name like "foo/bar".  The `<session>` in
> the above example command can be the local branch name of the clien-
> side, such as "my/topic".
>
> To support this kind of workflow in CGit, add a filter and a new
> handler.  The filter will check the prefix of the reference name, and
> if the command has a special reference name, the filter will add a
> specific tag (`exec_by_hook`) to the command.  Commands with this
> specific tag will be executed by a new handler (an external hook named
> "execute-commands") instead of the internal `execute_commands` function.

I do not claim to be great at naming, but you are worse ;-)

 - Any hook is about executing command(s), so "execute-commands"
   hook does not give any information to users.

 - IIUC, this is only about what happens when accepting a push and
   is not called at any other time.  Naming your hook without
   "receive" anywhere in its name would mean other people won't be
   able to add hook that "executes" commands upon cues other than
   receiving a push.

I can guess why you chose that name, because I know there is a
function called execute_commands() in "git receive-pack", but that
is not somethhing you can expect your end users, who are not
intimate to our codebase, to know.

> We can use the external "execute-commands" hook to create pull requests
> or send emails.

You can create pull requests or send emails out of the post-receive
hook, so that is not a convincing justification why we want a new
hook.

Now, I understand that Gerrit-style "notice a push to for/<target>,
take over the whole operation that happens after receiving the pack
data and do something entirely different, such as attempting to run
a merge with refs/heads/<target> and update refs/heads/<target>
instead, or fail the push if automerge fails" is not easy to arrange
within the current "pre-receive" + "post-receive" framework (by the
way, we should start considering to deprecate "update", and
"post-update" hooks as these "*-receive" hooks were added to replace
them, perhaps we should leave a #leftoverbits mark here).  And I
think it is reasonable to add a new hook that takes over the whole
flow in "git receive-pack" to do so.

I just do not think "the execute-commands hook" is a good name for
it.  Perhaps "divert-receive" (as it diverts large portion of what
receive does) or something?  I dunno.

How do Gerrit folks deal with the "we pushed to the server, so let's
pretend to have turned around and fetched from the same server
immediately after doing so" client-side hack, by the way?  

A vanilla "git push" on the client side does not know a push to
refs/for/master would result in an update to refs/heads/master on
the server side, and it would not know the result of the munging
done on the server side (whether it is to rebase what is received on
top of 'master' or to merge it to 'master') anyway, the
remote-tracking branch refs/remotes/origin/master on the client side
would be left stale.  If we wanted to help them pretend to have
fetched immediately after, I think we need to extend the protocol.
Right now, after accepting "git push", the server end will say, for
each proposed update for a ref, if the push updated successfully or
not, but to support the "push to for/<target>, get heads/<target>
updated" interaction, the reporting of the result (done in the
report() function in builtin/receive-pack.c) needs to be able to say
what ref (it may be a ref that "git push" did not think it pushed
to) got updated to what value (it may be an object the client does
not yet have---and we may have to actually turn around and fetch
from them internally if we want to keep the illusion).



^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH 0/7] New execute-commands hook for centralized workflow
  2020-03-04 20:39 ` [PATCH 0/7] New execute-commands hook for centralized workflow Junio C Hamano
@ 2020-03-05 16:51   ` Jiang Xin
  2020-03-08 14:56     ` [PATCH v2 0/5] New proc-receive " Jiang Xin
                       ` (5 more replies)
  0 siblings, 6 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-05 16:51 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年3月5日周四 上午4:39写道:
>
> I do not claim to be great at naming, but you are worse ;-)

I totally agree that I am not good at naming, for example my daughter's name.

>  - Any hook is about executing command(s), so "execute-commands"
>    hook does not give any information to users.
>
>  - IIUC, this is only about what happens when accepting a push and
>    is not called at any other time.  Naming your hook without
>    "receive" anywhere in its name would mean other people won't be
>    able to add hook that "executes" commands upon cues other than
>    receiving a push.
>
> I can guess why you chose that name, because I know there is a
> function called execute_commands() in "git receive-pack", but that
> is not somethhing you can expect your end users, who are not
> intimate to our codebase, to know.

Yes, it's better to name the hook "* -receive", because the hooks are
for different commands, such as "commit-msg" is for `git commit`.

> > We can use the external "execute-commands" hook to create pull requests
> > or send emails.
>
> You can create pull requests or send emails out of the post-receive
> hook, so that is not a convincing justification why we want a new
> hook.

Another solution is using "pre-receive" + "post-receive" to handle a
push to "refs/for/master".  The "post-receive" hook is used to create
a pull requst and delete the special reference "refs/for/master"
created between these two hooks.  But having a temporary reference
created is not safe for concurrent pushes.

> Now, I understand that Gerrit-style "notice a push to for/<target>,
> take over the whole operation that happens after receiving the pack
> data and do something entirely different, such as attempting to run
> a merge with refs/heads/<target> and update refs/heads/<target>
> instead, or fail the push if automerge fails" is not easy to arrange
> within the current "pre-receive" + "post-receive" framework (by the
> way, we should start considering to deprecate "update", and
> "post-update" hooks as these "*-receive" hooks were added to replace
> them, perhaps we should leave a #leftoverbits mark here).  And I
> think it is reasonable to add a new hook that takes over the whole
> flow in "git receive-pack" to do so.
>
> I just do not think "the execute-commands hook" is a good name for
> it.  Perhaps "divert-receive" (as it diverts large portion of what
> receive does) or something?  I dunno.

I suggest naming the hook as "process-receive", which is executed
between the other two "p*-receive" hooks, and no need to create a
special "pre-receive" for "process-receive".

> How do Gerrit folks deal with the "we pushed to the server, so let's
> pretend to have turned around and fetched from the same server
> immediately after doing so" client-side hack, by the way?

In the following example, I push a local commit to a special reference
(refs/for/master) of the remote Gerrit server.  The "report()"
function (if Gerrit has one) says a new reference "refs/for/master"
has been created.  But in deed, there is no such reference created in
the remote repository, Gerrit will create another reference instead,
such as "refs/changes/71/623871/1", for user to download the code
review .  Because the local repository only has normal
"remote.<name>.fetch" config variables for remote tracking, so git
will not create a tracking reference for "refs/for/master".  Command
line tool, such as Android "repo" (or the reimplemented git-repo in
Golang), will create a special reference
(refs/published/<local/branch>) for tracking, and these tools are
responsible for banch tracking.

    $ git push --receive-pack="gerrit receive-pack" origin
refs/heads/master:refs/for/master
    Enumerating objects: 13, done.
    Counting objects: 100% (13/13), done.
    Delta compression using up to 8 threads
    Compressing objects: 100% (11/11), done.
    Writing objects: 100% (12/12), 1.34 KiB | 171.00 KiB/s, done.
    Total 12 (delta 2), reused 0 (delta 0), pack-reused 0
    remote: Resolving deltas: 100% (2/2)
    remote: Processing changes: refs: 1, new: 1, done
    remote:
    remote: SUCCESS
    remote:
    remote: New Changes:
    remote:   http://gerrit.example.com/c/my/repo/+/623889 Test commit
    To ssh://gerrit.example.com:29418/my/repo
     * [new branch]      master -> refs/for/master


> A vanilla "git push" on the client side does not know a push to
> refs/for/master would result in an update to refs/heads/master on
> the server side, and it would not know the result of the munging
> done on the server side (whether it is to rebase what is received on
> top of 'master' or to merge it to 'master') anyway, the

Neither Gerrit nor our AGit-Flow server will update the master branch.
Our AGit-Flow server will create a special reference (like GitHub's
"refs/pull/<number>/head") for reviewers to download commits.

> remote-tracking branch refs/remotes/origin/master on the client side
> would be left stale.  If we wanted to help them pretend to have
> fetched immediately after, I think we need to extend the protocol.
> Right now, after accepting "git push", the server end will say, for
> each proposed update for a ref, if the push updated successfully or
> not, but to support the "push to for/<target>, get heads/<target>
> updated" interaction, the reporting of the result (done in the
> report() function in builtin/receive-pack.c) needs to be able to say
> what ref (it may be a ref that "git push" did not think it pushed
> to) got updated to what value (it may be an object the client does
> not yet have---and we may have to actually turn around and fetch
> from them internally if we want to keep the illusion).

I have no idea now how to make a simple patch to give an accurate report.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v2 0/5] New proc-receive hook for centralized workflow
  2020-03-05 16:51   ` Jiang Xin
@ 2020-03-08 14:56     ` Jiang Xin
  2020-03-08 14:56     ` [PATCH v2 1/5] receive-pack: add new proc-receive hook Jiang Xin
                       ` (4 subsequent siblings)
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-08 14:56 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Changes since v1:

* Use a better name "proc-receive" for the new hook.
* Rename the new config variable to "receive.procReceiveRefs".

--

Jiang Xin (5):
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  receive-pack: read env from proc-receive output
  hook: add document and example for "proc-receive" hook

 Documentation/config/receive.txt     |  14 +
 Documentation/githooks.txt           |  22 +
 builtin/receive-pack.c               | 160 +++++-
 refs.c                               |  11 +-
 refs.h                               |   1 +
 t/t5411-proc-receive-hook.sh         | 831 +++++++++++++++++++++++++++
 templates/hooks--proc-receive.sample | 119 ++++
 7 files changed, 1141 insertions(+), 17 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100755 templates/hooks--proc-receive.sample

-- 
2.26.0.rc0.5.gb02b988a14.dirty


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v2 1/5] receive-pack: add new proc-receive hook
  2020-03-05 16:51   ` Jiang Xin
  2020-03-08 14:56     ` [PATCH v2 0/5] New proc-receive " Jiang Xin
@ 2020-03-08 14:56     ` Jiang Xin
  2020-03-09 17:12       ` Junio C Hamano
  2020-03-08 14:56     ` [PATCH v2 2/5] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                       ` (3 subsequent siblings)
  5 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-03-08 14:56 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`have_special_ref`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

This "proc-receive" hook executes once for the receive operation. It
takes no arguments, but gets the same information as the pre-receive
hook does on its standard input.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       |  93 +++++-
 t/t5411-proc-receive-hook.sh | 547 +++++++++++++++++++++++++++++++++++
 2 files changed, 628 insertions(+), 12 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..23d0c224d2 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -312,7 +312,8 @@ struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     have_special_ref:1;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -669,6 +670,9 @@ static void prepare_push_cert_sha1(struct child_process *proc)
 
 struct receive_hook_feed_state {
 	struct command *cmd;
+	int feed_normal_ref;
+	int feed_special_ref;
+	int hook_must_exist;
 	int skip_broken;
 	struct strbuf buf;
 	const struct string_list *push_options;
@@ -684,8 +688,14 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
 	int code;
 
 	argv[0] = find_hook(hook_name);
-	if (!argv[0])
-		return 0;
+	if (!argv[0]) {
+		if (feed_state->hook_must_exist) {
+			rp_error("cannot to find hook '%s'", hook_name);
+			return 1;
+		} else {
+			return 0;
+		}
+	}
 
 	argv[1] = NULL;
 
@@ -751,9 +761,17 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
 
-	while (cmd &&
-	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
-		cmd = cmd->next;
+	while (cmd) {
+		if (state->skip_broken && (cmd->error_string || cmd->did_not_exist))
+			cmd = cmd->next;
+		else if (state->feed_special_ref && cmd->have_special_ref)
+			break;
+		else if (state->feed_normal_ref && !cmd->have_special_ref)
+			break;
+		else
+			cmd = cmd->next;
+	}
+
 	if (!cmd)
 		return -1; /* EOF */
 	strbuf_reset(&state->buf);
@@ -778,6 +796,9 @@ static int run_receive_hook(struct command *commands,
 
 	strbuf_init(&state.buf, 0);
 	state.cmd = commands;
+	state.feed_normal_ref = 1;
+	state.feed_special_ref = 1;
+	state.hook_must_exist = 0;
 	state.skip_broken = skip_broken;
 	if (feed_receive_hook(&state, NULL, NULL))
 		return 0;
@@ -817,6 +838,27 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct receive_hook_feed_state state;
+	int status;
+
+	strbuf_init(&state.buf, 0);
+	state.cmd = commands;
+	state.feed_normal_ref = 0;
+	state.feed_special_ref = 1;
+	state.hook_must_exist = 1;
+	state.skip_broken = 1;
+	if (feed_receive_hook(&state, NULL, NULL))
+		return 0;
+	state.cmd = commands;
+	state.push_options = push_options;
+	status = run_and_feed_hook("proc-receive", feed_receive_hook, &state);
+	strbuf_release(&state.buf);
+	return status;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1434,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->have_special_ref)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1474,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->have_special_ref)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1510,8 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int have_special_ref = 0;
+	int have_normal_ref = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1541,22 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->have_special_ref = 1;
+			have_special_ref = 1;
+		} else {
+			have_normal_ref = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,10 +1583,19 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
-	if (use_atomic)
-		execute_commands_atomic(commands, si);
-	else
-		execute_commands_non_atomic(commands, si);
+	if (have_special_ref && run_proc_receive_hook(commands, push_options)) {
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!cmd->error_string  && (cmd->have_special_ref || use_atomic))
+				cmd->error_string = "fail to run proc-receive hook";
+		}
+	}
+
+	if (have_normal_ref) {
+		if (use_atomic)
+			execute_commands_atomic(commands, si);
+		else
+			execute_commands_non_atomic(commands, si);
+	}
 
 	if (shallow_update)
 		warn_if_skipped_connectivity_check(commands, si);
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..01f67965bf
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,547 @@
+#!/bin/sh
+#
+# Copyright (c) 2018-2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook on special git-push refspec'
+
+. ./test-lib.sh
+
+bare=bare.git
+
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} 2>/dev/null)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+test_expect_success "setup repository" '
+	git init --bare $bare &&
+
+	# Enable push options for bare.git.
+	git -C $bare config receive.advertisePushOptions true &&
+
+	git clone --no-local $bare work &&
+	create_commits_in work A B
+'
+
+test_expect_success "setup hooks" '
+	## pre-receive hook
+	cat >$bare/hooks/pre-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: pre-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	EOF
+
+	## proc-receive hook
+	cat >$bare/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: proc-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	EOF
+
+	## post-receive hook
+	cat >$bare/hooks/post-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: post-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+	EOF
+	chmod a+x \
+		$bare/hooks/pre-receive \
+		$bare/hooks/post-receive \
+		$bare/hooks/proc-receive \
+'
+
+test_expect_success "(1) standard git-push command" '
+	(
+		cd work &&
+		git update-ref HEAD $A &&
+		git push origin HEAD HEAD:maint HEAD:a/b/c 2>&1
+	) >out &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/maint.
+	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/a/b/c.
+	remote: execute: post-receive hook
+	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/maint.
+	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/a/b/c.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(1) push one special ref" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		git push origin HEAD:refs/for/master/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(1) push both a normal and a special refs" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$B refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(1) cleanup" '
+	(
+		cd $bare &&
+		git update-ref refs/heads/master $A $B &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) remove proc-receive hook" '
+	mv $bare/hooks/proc-receive $bare/hooks/proc-receive.ok
+'
+
+test_expect_success "(2) standard git-push command" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		git push origin HEAD:master 2>&1
+	) >out &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: execute: post-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$B refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) cleanup" '
+	(
+		cd $bare &&
+		git update-ref refs/heads/master $A $B &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) push one special ref (failed)" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		test_must_fail git push origin HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/a/b/c/my/topic.
+	remote: error: cannot to find hook '"'"'proc-receive'"'"'
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) push both a normal and a special refs (one failed)" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		test_must_fail git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: error: cannot to find hook '"'"'proc-receive'"'"'
+	remote: execute: post-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$B refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) cleanup" '
+	(
+		cd $bare &&
+		git update-ref refs/heads/master $A $B &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) atomic push both a normal and a special refs (failed)" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		test_must_fail git push --atomic origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: error: cannot to find hook '"'"'proc-receive'"'"'
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(3) new proc-receive hook (return error)" '
+	cat >$bare/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: proc-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	printf >&2 "fail to run proc-receive\n"
+	exit 1
+	EOF
+	chmod a+x $bare/hooks/proc-receive
+'
+
+test_expect_success "(3) standard git-push command" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		git push origin HEAD:master 2>&1
+	) >out &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: execute: post-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$B refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(3) cleanup" '
+	(
+		cd $bare &&
+		git update-ref refs/heads/master $A $B &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(3) push both a normal and a special refs (one failed)" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		test_must_fail git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: fail to run proc-receive
+	remote: execute: post-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$B refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(3) cleanup" '
+	(
+		cd $bare &&
+		git update-ref refs/heads/master $A $B &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(3) atomic push a normal and a special refs (failed)" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		test_must_fail git push --atomic origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: fail to run proc-receive
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(4) restore proc-receive hook" '
+	mv $bare/hooks/proc-receive $bare/hooks/proc-receive.fail &&
+	mv $bare/hooks/proc-receive.ok $bare/hooks/proc-receive
+'
+
+test_expect_success "(4) push two special references" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/a/b/c/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/a/b/c/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/a/b/c/my/topic.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(5) new pre-receive hook hook (return error)" '
+	mv $bare/hooks/pre-receive $bare/hooks/pre-receive.ok &&
+	cat >$bare/hooks/pre-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: pre-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	printf >&2 ">> ERROR: declined in pre-receive hook\n"
+	exit 1
+	EOF
+	chmod a+x $bare/hooks/pre-receive
+'
+
+test_expect_success "(5) push two special references (declined)" '
+	(
+		cd work &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/my/topic \
+			HEAD:refs/for/maint/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: >> ERROR: declined in pre-receive hook
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(5) push both a normal and a special refs (declined)" '
+	(
+		cd work &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/my/topic \
+			HEAD:refs/heads/master
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $A, new: $B, ref: refs/heads/master.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: >> ERROR: declined in pre-receive hook
+	EOF
+	test_cmp expect actual
+'
+
+test_done
-- 
2.26.0.rc0.5.gb02b988a14.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 2/5] refs.c: refactor to reuse ref_is_hidden()
  2020-03-05 16:51   ` Jiang Xin
  2020-03-08 14:56     ` [PATCH v2 0/5] New proc-receive " Jiang Xin
  2020-03-08 14:56     ` [PATCH v2 1/5] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-03-08 14:56     ` Jiang Xin
  2020-03-08 15:38     ` [PATCH v2 3/5] receive-pack: new config receive.procReceiveRefs Jiang Xin
                       ` (2 subsequent siblings)
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-08 14:56 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.26.0.rc0.5.gb02b988a14.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 3/5] receive-pack: new config receive.procReceiveRefs
  2020-03-05 16:51   ` Jiang Xin
                       ` (2 preceding siblings ...)
  2020-03-08 14:56     ` [PATCH v2 2/5] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-03-08 15:38     ` Jiang Xin
  2020-03-08 15:38     ` [PATCH v2 4/5] receive-pack: read env from proc-receive output Jiang Xin
  2020-03-08 15:38     ` [PATCH v2 5/5] hook: add document and example for "proc-receive" hook Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-08 15:38 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 +++
 builtin/receive-pack.c           |  49 ++++++--
 t/t5411-proc-receive-hook.sh     | 189 +++++++++++++++++++++++++++----
 3 files changed, 220 insertions(+), 32 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..0178f2d478 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for/", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 23d0c224d2..5aff682758 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1544,17 +1559,30 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->have_special_ref = 1;
-			have_special_ref = 1;
-		} else {
-			have_normal_ref = 1;
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->have_special_ref = 1;
+				have_special_ref = 1;
+			} else {
+				have_normal_ref = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
+	} else {
+		have_normal_ref = 1;
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2011,6 +2039,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2126,5 +2156,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 01f67965bf..e3bb421078 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -91,17 +91,77 @@ test_expect_success "(1) standard git-push command" '
 	(
 		cd work &&
 		git update-ref HEAD $A &&
-		git push origin HEAD HEAD:maint HEAD:a/b/c 2>&1
+		git push origin HEAD HEAD:maint 2>&1
 	) >out &&
 	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
 	cat >expect <<-EOF &&
 	remote: execute: pre-receive hook
 	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/master.
 	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/maint.
-	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/a/b/c.
 	remote: execute: post-receive hook
 	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/master.
 	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/maint.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(1) push one special ref (create one)" '
+	(
+		cd work &&
+		git update-ref HEAD $B &&
+		git push origin HEAD:refs/for/master/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$B refs/for/master/my/topic
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(1) cleanup" '
+	(
+		cd $bare &&
+		git update-ref -d refs/for/master/my/topic
+	)
+'
+
+test_expect_success "(2) add config to turn on proc-receive" '
+	git -C $bare config --add receive.procReceiveRefs refs/for/
+'
+
+test_expect_success "(2) standard git-push command" '
+	(
+		cd work &&
+		git update-ref HEAD $A &&
+		git push origin HEAD:a/b/c 2>&1
+	) >out &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/a/b/c.
+	remote: execute: post-receive hook
 	remote: >> old: $ZERO_OID, new: $A, ref: refs/heads/a/b/c.
 	EOF
 	test_cmp expect actual &&
@@ -117,7 +177,7 @@ test_expect_success "(1) standard git-push command" '
 	test_cmp expect actual
 '
 
-test_expect_success "(1) push one special ref" '
+test_expect_success "(2) push one special ref" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -145,9 +205,10 @@ test_expect_success "(1) push one special ref" '
 	test_cmp expect actual
 '
 
-test_expect_success "(1) push both a normal and a special refs" '
+test_expect_success "(2) push both a normal and a special refs" '
 	(
 		cd work &&
+		git update-ref HEAD $B &&
 		git push origin \
 			HEAD:refs/for/maint/my/topic \
 			HEAD:refs/heads/master
@@ -176,7 +237,7 @@ test_expect_success "(1) push both a normal and a special refs" '
 	test_cmp expect actual
 '
 
-test_expect_success "(1) cleanup" '
+test_expect_success "(2) cleanup" '
 	(
 		cd $bare &&
 		git update-ref refs/heads/master $A $B &&
@@ -190,11 +251,93 @@ test_expect_success "(1) cleanup" '
 	test_cmp expect actual
 '
 
-test_expect_success "(2) remove proc-receive hook" '
+test_expect_success "(2) push two special references (one is not registered)" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/maint/my/topic \
+			HEAD:refs/drafts/maint/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/drafts/maint/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/maint/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/drafts/maint/my/topic.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$B refs/drafts/maint/my/topic
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) cleanup" '
+	(
+		cd $bare &&
+		git update-ref -d refs/drafts/maint/my/topic &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(2) register new ref prefix" '
+	git -C $bare config --add receive.procReceiveRefs refs/drafts/
+'
+
+test_expect_success "(2) push to two special references (all registered)" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/master/my/topic \
+			HEAD:refs/drafts/maint/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/drafts/maint/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/drafts/maint/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/drafts/maint/my/topic.
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(3) remove proc-receive hook" '
 	mv $bare/hooks/proc-receive $bare/hooks/proc-receive.ok
 '
 
-test_expect_success "(2) standard git-push command" '
+test_expect_success "(3) standard git-push command" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -220,7 +363,7 @@ test_expect_success "(2) standard git-push command" '
 	test_cmp expect actual
 '
 
-test_expect_success "(2) cleanup" '
+test_expect_success "(3) cleanup" '
 	(
 		cd $bare &&
 		git update-ref refs/heads/master $A $B &&
@@ -234,7 +377,7 @@ test_expect_success "(2) cleanup" '
 	test_cmp expect actual
 '
 
-test_expect_success "(2) push one special ref (failed)" '
+test_expect_success "(3) push one special ref (failed)" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -249,7 +392,7 @@ test_expect_success "(2) push one special ref (failed)" '
 	test_cmp expect actual
 '
 
-test_expect_success "(2) push both a normal and a special refs (one failed)" '
+test_expect_success "(3) push both a normal and a special refs (one failed)" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -279,7 +422,7 @@ test_expect_success "(2) push both a normal and a special refs (one failed)" '
 	test_cmp expect actual
 '
 
-test_expect_success "(2) cleanup" '
+test_expect_success "(3) cleanup" '
 	(
 		cd $bare &&
 		git update-ref refs/heads/master $A $B &&
@@ -293,7 +436,7 @@ test_expect_success "(2) cleanup" '
 	test_cmp expect actual
 '
 
-test_expect_success "(2) atomic push both a normal and a special refs (failed)" '
+test_expect_success "(3) atomic push both a normal and a special refs (failed)" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -321,7 +464,7 @@ test_expect_success "(2) atomic push both a normal and a special refs (failed)"
 	test_cmp expect actual
 '
 
-test_expect_success "(3) new proc-receive hook (return error)" '
+test_expect_success "(4) new proc-receive hook (return error)" '
 	cat >$bare/hooks/proc-receive <<-EOF &&
 	#!/bin/sh
 
@@ -338,7 +481,7 @@ test_expect_success "(3) new proc-receive hook (return error)" '
 	chmod a+x $bare/hooks/proc-receive
 '
 
-test_expect_success "(3) standard git-push command" '
+test_expect_success "(4) standard git-push command" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -364,7 +507,7 @@ test_expect_success "(3) standard git-push command" '
 	test_cmp expect actual
 '
 
-test_expect_success "(3) cleanup" '
+test_expect_success "(4) cleanup" '
 	(
 		cd $bare &&
 		git update-ref refs/heads/master $A $B &&
@@ -378,7 +521,7 @@ test_expect_success "(3) cleanup" '
 	test_cmp expect actual
 '
 
-test_expect_success "(3) push both a normal and a special refs (one failed)" '
+test_expect_success "(4) push both a normal and a special refs (one failed)" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -410,7 +553,7 @@ test_expect_success "(3) push both a normal and a special refs (one failed)" '
 	test_cmp expect actual
 '
 
-test_expect_success "(3) cleanup" '
+test_expect_success "(4) cleanup" '
 	(
 		cd $bare &&
 		git update-ref refs/heads/master $A $B &&
@@ -424,7 +567,7 @@ test_expect_success "(3) cleanup" '
 	test_cmp expect actual
 '
 
-test_expect_success "(3) atomic push a normal and a special refs (failed)" '
+test_expect_success "(4) atomic push a normal and a special refs (failed)" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -454,12 +597,12 @@ test_expect_success "(3) atomic push a normal and a special refs (failed)" '
 	test_cmp expect actual
 '
 
-test_expect_success "(4) restore proc-receive hook" '
+test_expect_success "(5) restore proc-receive hook" '
 	mv $bare/hooks/proc-receive $bare/hooks/proc-receive.fail &&
 	mv $bare/hooks/proc-receive.ok $bare/hooks/proc-receive
 '
 
-test_expect_success "(4) push two special references" '
+test_expect_success "(5) push two special references" '
 	(
 		cd work &&
 		git update-ref HEAD $B &&
@@ -492,7 +635,7 @@ test_expect_success "(4) push two special references" '
 	test_cmp expect actual
 '
 
-test_expect_success "(5) new pre-receive hook hook (return error)" '
+test_expect_success "(6) new pre-receive hook hook (return error)" '
 	mv $bare/hooks/pre-receive $bare/hooks/pre-receive.ok &&
 	cat >$bare/hooks/pre-receive <<-EOF &&
 	#!/bin/sh
@@ -510,7 +653,7 @@ test_expect_success "(5) new pre-receive hook hook (return error)" '
 	chmod a+x $bare/hooks/pre-receive
 '
 
-test_expect_success "(5) push two special references (declined)" '
+test_expect_success "(6) push two special references (declined)" '
 	(
 		cd work &&
 		test_must_fail git push origin \
@@ -527,7 +670,7 @@ test_expect_success "(5) push two special references (declined)" '
 	test_cmp expect actual
 '
 
-test_expect_success "(5) push both a normal and a special refs (declined)" '
+test_expect_success "(6) push both a normal and a special refs (declined)" '
 	(
 		cd work &&
 		test_must_fail git push origin \
-- 
2.26.0.rc0.5.gb02b988a14.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 4/5] receive-pack: read env from proc-receive output
  2020-03-05 16:51   ` Jiang Xin
                       ` (3 preceding siblings ...)
  2020-03-08 15:38     ` [PATCH v2 3/5] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-03-08 15:38     ` Jiang Xin
  2020-03-08 15:38     ` [PATCH v2 5/5] hook: add document and example for "proc-receive" hook Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-08 15:38 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The “post-receive” hook may need the pull request ID generated by the
“proc-receive” hook.  The results can be passed between hooks by
environment variables.

Each line of the message received from the standard output of the
“proc-receive” hook in the key=value format is parsed as environment and
these variables will be sent to environment of the “post-receive” hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 38 ++++++++++++++++++--
 t/t5411-proc-receive-hook.sh | 70 ++++++++++++++++++++++++++++++++++++
 2 files changed, 105 insertions(+), 3 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 5aff682758..d854952410 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -87,6 +87,7 @@ static const char *nonce_status;
 static long nonce_stamp_slop;
 static timestamp_t nonce_stamp_slop_limit;
 static struct ref_transaction *transaction;
+struct argv_array post_receive_env_array;
 
 static enum {
 	KEEPALIVE_NEVER = 0,
@@ -694,7 +695,9 @@ struct receive_hook_feed_state {
 };
 
 typedef int (*feed_fn)(void *, const char **, size_t *);
+typedef void (*stdout_handler_fn)(int out);
 static int run_and_feed_hook(const char *hook_name, feed_fn feed,
+			     stdout_handler_fn stdout_handler,
 			     struct receive_hook_feed_state *feed_state)
 {
 	struct child_process proc = CHILD_PROCESS_INIT;
@@ -716,9 +719,15 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
 
 	proc.argv = argv;
 	proc.in = -1;
-	proc.stdout_to_stderr = 1;
+	if (stdout_handler)
+		proc.out = -1;
+	else
+		proc.stdout_to_stderr = 1;
 	proc.trace2_hook_name = hook_name;
 
+	if (!strcmp(hook_name, "post-receive") && post_receive_env_array.argc > 0)
+		argv_array_pushv(&proc.env_array, post_receive_env_array.argv);
+
 	if (feed_state->push_options) {
 		int i;
 		for (i = 0; i < feed_state->push_options->nr; i++)
@@ -763,6 +772,10 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
 			break;
 	}
 	close(proc.in);
+
+	if (stdout_handler)
+		stdout_handler(proc.out);
+
 	if (use_sideband)
 		finish_async(&muxer);
 
@@ -819,7 +832,7 @@ static int run_receive_hook(struct command *commands,
 		return 0;
 	state.cmd = commands;
 	state.push_options = push_options;
-	status = run_and_feed_hook(hook_name, feed_receive_hook, &state);
+	status = run_and_feed_hook(hook_name, feed_receive_hook, NULL, &state);
 	strbuf_release(&state.buf);
 	return status;
 }
@@ -853,6 +866,23 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static void prepare_post_receive_env(int in)
+{
+	struct strbuf stdout_buf = STRBUF_INIT;
+
+	while (strbuf_getwholeline_fd(&stdout_buf, in, '\n') != EOF) {
+		char *p = stdout_buf.buf + stdout_buf.len -1;
+		if (*p =='\n')
+			*p = '\0';
+		p = strchr(stdout_buf.buf, '=');
+		if (p == NULL)
+			continue;
+		argv_array_push(&post_receive_env_array, stdout_buf.buf);
+		strbuf_reset(&stdout_buf);
+	}
+	strbuf_release(&stdout_buf);
+}
+
 static int run_proc_receive_hook(struct command *commands,
 				 const struct string_list *push_options)
 {
@@ -869,7 +899,8 @@ static int run_proc_receive_hook(struct command *commands,
 		return 0;
 	state.cmd = commands;
 	state.push_options = push_options;
-	status = run_and_feed_hook("proc-receive", feed_receive_hook, &state);
+	status = run_and_feed_hook("proc-receive",
+			feed_receive_hook, prepare_post_receive_env, &state);
 	strbuf_release(&state.buf);
 	return status;
 }
@@ -2040,6 +2071,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	};
 
 	string_list_init(&proc_receive_refs, 0);
+	argv_array_init(&post_receive_env_array);
 
 	packet_trace_identity("receive-pack");
 
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index e3bb421078..6f35867e85 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -687,4 +687,74 @@ test_expect_success "(6) push both a normal and a special refs (declined)" '
 	test_cmp expect actual
 '
 
+test_expect_success "(7) restore pre-receive hook" '
+	mv $bare/hooks/pre-receive $bare/hooks/pre-receive.fail &&
+	mv $bare/hooks/pre-receive.ok $bare/hooks/pre-receive
+'
+
+test_expect_success "(7) new proc-receive and post-receive hooks (pass environment variables)" '
+	## proc-receive hook
+	mv $bare/hooks/proc-receive $bare/hooks/proc-receive.ok &&
+	cat >$bare/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: proc-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	printf "GIT_VAR1=var1\n"
+	printf "GIT_VAR2=var2\n"
+	printf "AGIT_VAR1=foo\n"
+	printf "AGIT_VAR2=bar\n"
+	EOF
+	chmod a+x $bare/hooks/proc-receive &&
+
+	## post-receive hook
+	mv $bare/hooks/post-receive $bare/hooks/post-receive.ok &&
+	cat >$bare/hooks/post-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "execute: post-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 ">> old: \$old, new: \$new, ref: \$ref.\n"
+	done
+
+	for k in GIT_VAR1 GIT_VAR2 AGIT_VAR1 AGIT_VAR2
+	do
+		if test -n "\$(eval echo \\"\\\$\$k\")"
+		then
+			printf >&2 ">> has env: \$k=\$(eval echo \\"\\\$\$k\").\n"
+		fi
+	done
+	EOF
+	chmod a+x $bare/hooks/post-receive
+'
+
+test_expect_success "(7) push one special ref (show environments)" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/master/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: execute: pre-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: execute: proc-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: execute: post-receive hook
+	remote: >> old: $ZERO_OID, new: $B, ref: refs/for/master/my/topic.
+	remote: >> has env: GIT_VAR1=var1.
+	remote: >> has env: GIT_VAR2=var2.
+	remote: >> has env: AGIT_VAR1=foo.
+	remote: >> has env: AGIT_VAR2=bar.
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.26.0.rc0.5.gb02b988a14.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 5/5] hook: add document and example for "proc-receive" hook
  2020-03-05 16:51   ` Jiang Xin
                       ` (4 preceding siblings ...)
  2020-03-08 15:38     ` [PATCH v2 4/5] receive-pack: read env from proc-receive output Jiang Xin
@ 2020-03-08 15:38     ` Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-08 15:38 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt           |  22 +++++
 t/t5411-proc-receive-hook.sh         |  71 ++++++++++++++++
 templates/hooks--proc-receive.sample | 119 +++++++++++++++++++++++++++
 3 files changed, 212 insertions(+)
 create mode 100755 templates/hooks--proc-receive.sample

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..1af213bad8 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,28 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  According to refnames of the commands which
+`git push` sends to 'git-receive-pack', the commands will be devided
+into two groups by matching what the `receive.procReceiveRefs`
+configuration variable defines.  One group of the commands will execute
+the internal `execute_commands` function to update the corresponding
+refnames, and the other group of commands which have matching refnames
+will execute this 'proc-receive' hook to create pull requests, etc.
+If there is no `receive.procReceiveRefs` settings, this hook won't
+execute at all, and all commands are sent to the internal
+`execute_commands` function.
+
+Its exit status only determines the success or failure of the group of
+commands with special refnames, unless atomic push is in use.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but gets the same information as the
+<<pre-receive,'pre-receive'>> hook does on its standard input.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 6f35867e85..caedd49f2e 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -757,4 +757,75 @@ test_expect_success "(7) push one special ref (show environments)" '
 	test_cmp expect actual
 '
 
+test_expect_success "(8) install proc-receive from proc-receive.sample" '
+	mv $bare/hooks/pre-receive $bare/hooks/pre-receive.ok &&
+	mv $bare/hooks/post-receive $bare/hooks/post-receive.env &&
+	mv $bare/hooks/proc-receive $bare/hooks/proc-receive.env &&
+	cp ../../templates/hooks--proc-receive.sample $bare/hooks/proc-receive &&
+	chmod a+x $bare/hooks/proc-receive
+'
+
+test_expect_success "(8) proc-receive.sample: show push result" '
+	(
+		cd work &&
+		git push origin \
+			HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: [proc-receive] *******************************************************
+	remote: [proc-receive] * Pull request #12345678901 created/updated           *
+	remote: [proc-receive] * URL: https://... ...                                *
+	remote: [proc-receive] *******************************************************
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(8) proc-receive.sample: show debug info" '
+	(
+		cd work &&
+		git push -o debug=1 -o reviewers=user1,user2 \
+			origin \
+			HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: [DEBUG] [proc-receive] push-option: AGIT_DEBUG=1
+	remote: [DEBUG] [proc-receive] push-option: AGIT_REVIEWERS=user1,user2
+	remote: [DEBUG] [proc-receive] command from stdin: $ZERO_OID $B refs/for/a/b/c/my/topic
+	remote: [DEBUG] [proc-receive] call API (AGIT_PR_TARGET=a/b/c, AGIT_PR_TOPIC=)...
+	remote: [DEBUG] [proc-receive] parse API result, and get AGIT_PR_ID, etc.
+	remote: [proc-receive] *******************************************************
+	remote: [proc-receive] * Pull request #12345678901 created/updated           *
+	remote: [proc-receive] * URL: https://... ...                                *
+	remote: [proc-receive] *******************************************************
+	remote: [DEBUG] [proc-receive] output kv pairs to stdout for git to parse.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "(8) proc-receive.sample: fail to push non-exist branch" '
+	(
+		cd work &&
+		test_must_fail git push -o reviewers=user1,user2 \
+			origin \
+			HEAD:refs/for/a/b/x/my/topic
+	) >out 2>&1 &&
+	grep "^remote:" out | sed -e "s/  *\$//g" >actual &&
+	cat >expect <<-EOF &&
+	remote: [proc-receive] cannot find target branch from ref: refs/for/a/b/x/my/topic
+	EOF
+	test_cmp expect actual &&
+	(
+		cd $bare &&
+		git show-ref
+	) >actual &&
+	cat >expect <<-EOF &&
+	$A refs/heads/a/b/c
+	$A refs/heads/maint
+	$A refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
 test_done
diff --git a/templates/hooks--proc-receive.sample b/templates/hooks--proc-receive.sample
new file mode 100755
index 0000000000..31fa85ffe1
--- /dev/null
+++ b/templates/hooks--proc-receive.sample
@@ -0,0 +1,119 @@
+#!/bin/sh
+#
+# This is an  example hook script, DO NOT use it on production service.
+
+debug() {
+	case "$AGIT_DEBUG" in
+	"yes" | "true" | "1")
+		;;
+	*)
+		return
+	esac
+
+	echo >&2 "[DEBUG] $@"
+}
+
+# Parse push options
+if test -n "$GIT_PUSH_OPTION_COUNT"
+then
+	i=0
+	while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
+	do
+		eval "value=\$GIT_PUSH_OPTION_$i"
+		i=$((i + 1))
+
+		k=$(echo ${value%=*} | tr [a-z] [A-Z])
+		v=${value#*=}
+		if test -n "$v" && test -n "$k"
+		then
+			k="AGIT_$k"
+		else
+			continue
+		fi
+		eval "$k=$v"
+		debug "[proc-receive] push-option: $k=$v"
+	done
+fi
+
+# Read push commands.
+count=0
+while read old new refname
+do
+	debug "[proc-receive] command from stdin: $old $new $refname"
+	count=$(( count + 1 ))
+	# Only one special refname is allowed for each push
+	if test $count -gt 1
+	then
+		echo >&2 "[proc-receive]: cannot handle more than one push commands"
+		exit 1
+	fi
+
+	# Parse refname, and set envrionment
+	remains=
+	if test "${refname#refs/for/}" != "$refname"
+	then
+		AGIT_PR_IS_DRAFT=false
+		remains=${refname#refs/for/}
+	elif test "${refname#refs/drafts/}" != "$refname"
+	then
+		AGIT_PR_IS_DRAFT=true
+		remains=${refname#refs/drafts/}
+	else
+		echo >&2 "[proc-receive] unknown refname: $refname"
+		exit 1
+	fi
+
+	ref=
+	found_ref=
+	for i in $(echo $remains | tr "/" "\n")
+	do
+		if test -z "$ref"
+		then
+			ref=$i
+		else
+			ref=$ref/$i
+		fi
+		if git rev-parse --verify $ref -- 2>/dev/null
+		then
+			found_ref=yes
+			break
+		fi
+	done
+	if test -z "$found_ref"
+	then
+		echo >&2 "[proc-receive] cannot find target branch from ref: $refname"
+		exit 1
+	fi
+	AGIT_PR_TARGET=$ref
+	AGIT_PR_SOURCE=${remains#$ref/}
+done
+
+if test -z "$AGIT_PR_TARGET"
+then
+	echo >&2 "[proc-receive] fail to parse refname, no target found"
+	exit 1
+fi
+
+# Call API to generate code review.
+debug "[proc-receive] call API (AGIT_PR_TARGET=$AGIT_PR_TARGET, AGIT_PR_TOPIC=$AGIT_PR_TOPIC)..."
+
+# Parse result of API.
+debug "[proc-receive] parse API result, and get AGIT_PR_ID, etc."
+AGIT_PR_ID="12345678901"
+AGIT_PR_LOCAL_ID="23"
+
+# Show message.
+if test -n "$AGIT_PR_ID"
+then
+	echo >&2 "[proc-receive] *******************************************************"
+	echo >&2 "[proc-receive] * Pull request #$AGIT_PR_ID created/updated           *"
+	echo >&2 "[proc-receive] * URL: https://... ...                                *"
+	echo >&2 "[proc-receive] *******************************************************"
+fi
+
+# Show envs to stdout, and will be exported as envs for "post-receive" hook.
+debug "[proc-receive] output kv pairs to stdout for git to parse."
+echo "AGIT_PR_ID=$AGIT_PR_ID"
+echo "AGIT_PR_LOCAL_ID=$AGIT_PR_LOCAL_ID"
+
+exit 0
-- 
2.26.0.rc0.5.gb02b988a14.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v2 1/5] receive-pack: add new proc-receive hook
  2020-03-08 14:56     ` [PATCH v2 1/5] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-03-09 17:12       ` Junio C Hamano
  2020-03-10  6:03         ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-03-09 17:12 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

>  builtin/receive-pack.c       |  93 +++++-
>  t/t5411-proc-receive-hook.sh | 547 +++++++++++++++++++++++++++++++++++
>  2 files changed, 628 insertions(+), 12 deletions(-)
>  create mode 100755 t/t5411-proc-receive-hook.sh
>
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index 2cc18bbffd..23d0c224d2 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -312,7 +312,8 @@ struct command {
>  	struct command *next;
>  	const char *error_string;
>  	unsigned int skip_update:1,
> -		     did_not_exist:1;
> +		     did_not_exist:1,
> +		     have_special_ref:1;
>  	int index;
>  	struct object_id old_oid;
>  	struct object_id new_oid;
> @@ -669,6 +670,9 @@ static void prepare_push_cert_sha1(struct child_process *proc)
>  
>  struct receive_hook_feed_state {
>  	struct command *cmd;
> +	int feed_normal_ref;
> +	int feed_special_ref;
> +	int hook_must_exist;
>  	int skip_broken;
>  	struct strbuf buf;
>  	const struct string_list *push_options;
> @@ -684,8 +688,14 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed,
>  	int code;
>  
>  	argv[0] = find_hook(hook_name);
> -	if (!argv[0])
> -		return 0;
> +	if (!argv[0]) {
> +		if (feed_state->hook_must_exist) {
> +			rp_error("cannot to find hook '%s'", hook_name);
> +			return 1;
> +		} else {
> +			return 0;
> +		}
> +	}
>  
>  	argv[1] = NULL;

Why do you even need "must-exist" in the first place?  In other
parts of Git, when a hook does not exist, processing by that missing
hook is simply not done.  Why does it have to cause an error?

If there is a reasonable answer to the above question, then the
concept that hooks can be marked as "must exist -- otherwise it is
an error" would be independently useful and the support for that
concept should be split out as a separate step before this one, I
would think.

Also, I am not sure why you have to have one single "special" ref
hierarchy to be handled by the hook.  Why not feed everything and
let the hook decide?

I would have expected a design more along the lines of "if the hook
exists, then feed all the refs to the hook, let the hook do whatever
it wants and report what it did back to the receive-pack process, so
that the client side can learn what got accepted and what got
rejected (and possibly other things that may require protocol
extension later)".  One of the possible things the hook can report
may be "I did not do anything for this ref, please do whatever you
normally do".  That way, you do not need to introduce "there are two
classes of refs, normal and special"---that becomes an unused and
unnecessary concept.  For whichever ref the hook considers "normal",
it can use the mechanism to say "I want you to do the normal thing".
And when it sees refs/for/ prefix (or whatever is considered
"special" at the hook's discretion), it can do whatever it wants,
and report "OK, I ate it" or "No, I reject it" (which are only the
two things that can be returned back to the client within the
current protocol, IIUC).  We may later want to extend the mechanism
to allow richer response by the hook at the same time to extend the
on-the-wire protocol and then it may become possible for the hook to
take a push to refs/for/master, update refs/heads/master to the
commit (or to some other commit), and report "OK, I accepted it and
also updated refs/heads/master to this other commit".

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2 1/5] receive-pack: add new proc-receive hook
  2020-03-09 17:12       ` Junio C Hamano
@ 2020-03-10  6:03         ` Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
                             ` (4 more replies)
  0 siblings, 5 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-10  6:03 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年3月10日周二 上午1:12写道:
> Why do you even need "must-exist" in the first place?  In other
> parts of Git, when a hook does not exist, processing by that missing
> hook is simply not done.  Why does it have to cause an error?
>
> If there is a reasonable answer to the above question, then the
> concept that hooks can be marked as "must exist -- otherwise it is
> an error" would be independently useful and the support for that
> concept should be split out as a separate step before this one, I
> would think.
>
> Also, I am not sure why you have to have one single "special" ref
> hierarchy to be handled by the hook.  Why not feed everything and
> let the hook decide?
>
> I would have expected a design more along the lines of "if the hook
> exists, then feed all the refs to the hook, let the hook do whatever
> it wants and report what it did back to the receive-pack process, so
> that the client side can learn what got accepted and what got
> rejected (and possibly other things that may require protocol
> extension later)".  One of the possible things the hook can report
> may be "I did not do anything for this ref, please do whatever you
> normally do".  That way, you do not need to introduce "there are two
> classes of refs, normal and special"---that becomes an unused and
> unnecessary concept.  For whichever ref the hook considers "normal",
> it can use the mechanism to say "I want you to do the normal thing".
> And when it sees refs/for/ prefix (or whatever is considered
> "special" at the hook's discretion), it can do whatever it wants,
> and report "OK, I ate it" or "No, I reject it" (which are only the
> two things that can be returned back to the client within the
> current protocol, IIUC).  We may later want to extend the mechanism
> to allow richer response by the hook at the same time to extend the
> on-the-wire protocol and then it may become possible for the hook to
> take a push to refs/for/master, update refs/heads/master to the
> commit (or to some other commit), and report "OK, I accepted it and
> also updated refs/heads/master to this other commit".

In patch 3/5, I add a new config variable "receive.procReceiveRefs"
for developers to choose whether all of the commands or only a part of
them should pipe to the "proc-receive" hook. For example, a developer
may set the config variable on the server like this:

    git config --system --add receive.procReceiveRefs refs/

Then all commands will pipe to the "proc-receive" hook.

Think there may be thousands of commands in the initial stage for
setting up a new repository, and all of the commands are "normal"
(creating branches or tags). If the "proc-receive" hook has to handle
all of the "normal" commands by piping these commands to a "git
update-ref --stdin" process, or pass these commands back to
"receive-pack" by a new protocol, this will greatly impact the
performance. I think it's better to let the developer to choose by
using a config variable like "receive.procReceiveRefs".

Patch 4/5 (receive-pack: read env from proc-receive output) is
unnecessary, and extend the on-the-wire protocol is a good idea. The
protocol may using pkt-line format, and negotiation between
receive-pack (S) and the hook (H) like this:

    S: PKT-LINE(version=1\0side-band-64k ...)  # I'm not sure it is
necessary for the capabilities for all controllable server side
softwares.
    S: flush-pkt
    H: PKT-LINE(version=1\0side-band-64k ...)
    H: flush-pkt
    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt
    H: PKT-LINE( sideband< progress... > )
    H: ... ...
    H: PKT-LINE(old-oid new-oid ref OK) # OK, I ate it
    H: PKT-LINE(old-oid new-oid ref error-msg...) # NO, I reject it
    H: PKT-LINE(old-oid new-oid ref)  # Back to receive-pack to handle it
    H: ... ...
    H: flush-pkt

After piping the "special" commands to the "proc-receive" hook,
receive-pack should discard the "special" commands, and add the
commands back from the output of the "proc-receive" hook. Then
"receive-pack" will continue to execute the internal
"execute_commands" function for the unexecuted commands.

In order to have performance similar to the vanilla receive-pack, it
is necessary to keep the config variable and the concept of "normal"
and "special" references. If the config variable
"receive.procReceiveRefs" is defined on the server, but the
"proc-receive" hook does not exist, it's better give an error instead
of fall back to the internal "execute_commands" function or doing
nothing.

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v3 0/4] New proc-receive hook for centralized workflow
  2020-03-10  6:03         ` Jiang Xin
@ 2020-03-13 12:23           ` Jiang Xin
  2020-03-22 13:18             ` [PATCH v4 0/5] " Jiang Xin
                               ` (5 more replies)
  2020-03-13 12:23           ` [PATCH v3 1/4] receive-pack: add new proc-receive hook Jiang Xin
                             ` (3 subsequent siblings)
  4 siblings, 6 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-13 12:23 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Before writing test cases, I want to make sure whether it is on the
right direction.

Changes since v2:

* Implement a on-the-wire protocol, see commit 1/4.

* Refactor report() on both server and client side for reporting of
  proc-receive.

--

Jiang Xin (4):
  receive-pack: add new proc-receive hook
  receive-pack: refactor report for proc-receive
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs

 Documentation/config/receive.txt |  14 ++
 builtin/receive-pack.c           | 294 ++++++++++++++++++++++++++++++-
 refs.c                           |  11 +-
 refs.h                           |   1 +
 transport.c                      |  33 +++-
 5 files changed, 332 insertions(+), 21 deletions(-)

-- 
2.26.0.rc1.5.gca1e965b06


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v3 1/4] receive-pack: add new proc-receive hook
  2020-03-10  6:03         ` Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-03-13 12:23           ` Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 2/4] receive-pack: refactor report for proc-receive Jiang Xin
                             ` (2 subsequent siblings)
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-13 12:23 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`have_special_ref`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

This "proc-receive" hook reads commands, push-options (optional), and
send result using a protocol in pkt-line format.  In the following
example, The letter "S" stands for "receive-pack" and letter "H" stands
for the hook.

    S: PKT-LINE(version=1\0push-options ...)
    S: flush-pkt

    H: PKT-LINE(version=1\0push-options ...)
    H: flush-pkt

    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt

    # Optional, only if push-options is negotiated.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # OK, run this command successfully.
    H: PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    H: PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ft)

    H: ... ...
    H: flush-pkt

After receiving a command, the hook can create/update another alternate
reference.  For example, a command for a reference "refs/for/master" may
create a special reference, such as "refs/pull/123/head".  The alternate
reference can be returned from the result in an extensible format like
"<old-oid> <new-oid> <reference> <status> [<message>]".

The result will be stored in a command list, and "receive-pack" will use
the result to replace the commands that have specific `have_special_ref`
field turned on.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c | 244 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 239 insertions(+), 5 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..8cda2b9cf7 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -312,7 +312,8 @@ struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     have_special_ref:1;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +818,209 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static int read_proc_receive_result(struct packet_reader *reader,
+				    struct command **commands)
+{
+	struct command **tail = commands;
+	int code = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+			    reader->line);
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status)
+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+			    reader->line);
+		*status++ = '\0';
+
+		FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname));
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+		cmd->have_special_ref = 1;
+
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2)
+			die("protocol error: proc-receive has bad status '%s' for '%s'",
+			    status, reader->line);
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				cmd->error_string = xstrdup(msg);
+			else
+				cmd->error_string = "failed";
+			code = 1;
+		} else if (strcmp("ok", status)) {
+			rp_warning("unknown proc-receive status '%s' for '%s'",
+				   status, reader->line);
+			cmd->error_string = "bad status";
+			code = 1;
+		}
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command **commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *result_commands = NULL;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot to find hook 'proc-receive'");
+		return 1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	packet_write_fmt(proc.in, "version=1%c%s\n",
+			 '\0',
+			 use_push_options ? "push-options": "");
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line+8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1)
+		die("protocol error: unknown proc-receive version '%d'", version);
+
+	/* Send commands */
+	for (cmd = *commands; cmd; cmd = cmd->next) {
+		char *old_hex, *new_hex;
+
+		if (!cmd->have_special_ref || cmd->skip_update ||
+		    cmd->did_not_exist || cmd->error_string)
+			continue;
+
+		old_hex = oid_to_hex(&cmd->old_oid);
+		new_hex = oid_to_hex(&cmd->new_oid);
+
+		packet_write_fmt(proc.in, "%s %s %s",
+				 old_hex, new_hex, cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_result(&reader, &result_commands);
+	close(proc.in);
+	close(proc.out);
+	if (finish_command(&proc))
+		die("proc-receive did not exit properly");
+
+	/* After receiving the result from the "proc-receive" hook,
+	 * "receive-pack" will use the result to replace commands that
+	 * have specific `have_special_ref` field.
+	 */
+	for (cmd = *commands; cmd; cmd = cmd->next)
+		if (!cmd->have_special_ref)
+			break;
+
+	if (!cmd || cmd->have_special_ref) {
+		cmd = result_commands;
+	} else {
+		struct command *tail = cmd;
+		for (;;) {
+			if (!tail->next)
+			       break;
+			else if (tail->next->have_special_ref)
+				tail->next = tail->next->next;
+			else
+				tail = tail->next;
+		}
+		tail->next = result_commands;
+	}
+	*commands = cmd;
+
+	// TODO: sort commands or not?
+
+	if (use_sideband)
+		finish_async(&muxer);
+
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1596,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->have_special_ref)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1636,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->have_special_ref)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1458,16 +1662,19 @@ static void execute_commands_atomic(struct command *commands,
 	strbuf_release(&err);
 }
 
-static void execute_commands(struct command *commands,
+static void execute_commands(struct command **orig_commands,
 			     const char *unpacker_error,
 			     struct shallow_info *si,
 			     const struct string_list *push_options)
 {
+	struct command *commands = *orig_commands;
 	struct check_connected_options opt = CHECK_CONNECTED_INIT;
 	struct command *cmd;
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int have_special_ref = 0;
+	int have_normal_ref = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1704,22 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->have_special_ref = 1;
+			have_special_ref = 1;
+		} else {
+			have_normal_ref = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1746,17 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (have_special_ref) {
+		if (run_proc_receive_hook(orig_commands, push_options)) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->have_special_ref || use_atomic))
+					cmd->error_string = "fail to run proc-receive hook";
+			}
+		} else {
+			commands = *orig_commands;
+		}
+	}
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -2019,7 +2253,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 			update_shallow_info(commands, &si, &ref);
 		}
 		use_keepalive = KEEPALIVE_ALWAYS;
-		execute_commands(commands, unpack_status, &si,
+		execute_commands(&commands, unpack_status, &si,
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
-- 
2.26.0.rc1.5.gca1e965b06


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 2/4] receive-pack: refactor report for proc-receive
  2020-03-10  6:03         ` Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 1/4] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-03-13 12:23           ` Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 3/4] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 4/4] receive-pack: new config receive.procReceiveRefs Jiang Xin
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-13 12:23 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

The "proc-receive" may update one or more references, and will send its
result one by one in pkt-line format.  Each line of the result has four
fields and one optional message field, as "<old-oid> <new-oid> <ref>
<status> [<message>]".  See the following example:

    # OK, run this command successfully.
    PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference.
    PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute.
    PKT-LINE(old-oid new-oid ref ft)

The first three fields have the same foramt as a command.

The forth field has a two-letter status code.  Available status code:

* ok: The command runs successfully.  If the optional message has a
  prefix "ref:", the hook has created/updated an alternate reference
  instead.

* ng: Fail to run the command. Error message is in the optional message
  field.

* ft: Will fallthrough to receive-pack to execute.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c | 21 ++++++++++++++++-----
 transport.c            | 33 ++++++++++++++++++++++++---------
 2 files changed, 40 insertions(+), 14 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 8cda2b9cf7..23f1ae3795 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -311,6 +311,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extra_string;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     have_special_ref:1;
@@ -868,7 +869,12 @@ static int read_proc_receive_result(struct packet_reader *reader,
 			else
 				cmd->error_string = "failed";
 			code = 1;
-		} else if (strcmp("ok", status)) {
+		} else if (!strcmp("ok", status)) {
+			cmd->extra_string = xstrdup_or_null(msg);
+		} else if (!strcmp("ft", status)) {
+			/* Unset "have_special_ref" field, will execute by "receive-pack" */
+			cmd->have_special_ref = 0;
+		} else {
 			rp_warning("unknown proc-receive status '%s' for '%s'",
 				   status, reader->line);
 			cmd->error_string = "bad status";
@@ -2133,12 +2139,17 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
+		if (!cmd->error_string) {
+			if (!cmd->extra_string)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			else
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, ' ', cmd->extra_string);
+		} else {
 			packet_buf_write(&buf, "ng %s %s\n",
 					 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..a4f2a5cc6f 100644
--- a/transport.c
+++ b/transport.c
@@ -463,11 +463,17 @@ static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	char *to_name = to->name;
+
+	if (to->remote_status) {
+		if (!strncmp("ref:", to->remote_status, 4))
+			to_name = to->remote_status + 4;
+	}
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +487,9 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -498,12 +504,21 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
 				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+	else if (is_null_oid(&ref->old_oid)) {
+		char *refname, *summary;
+		if (ref->remote_status && !strncmp(ref->remote_status, "ref:", 4))
+			refname = ref->remote_status + 4;
+		else
+			refname = ref->name;
+		if (starts_with(refname, "refs/tags/"))
+		       summary = "[new tag]";
+		else if (starts_with(refname, "refs/heads/"))
+		       summary = "[new branch]";
+		else
+		       summary = "[new reference]";
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
-	else {
+			summary, ref, ref->peer_ref, NULL, porcelain, summary_width);
+	} else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
-- 
2.26.0.rc1.5.gca1e965b06


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 3/4] refs.c: refactor to reuse ref_is_hidden()
  2020-03-10  6:03         ` Jiang Xin
                             ` (2 preceding siblings ...)
  2020-03-13 12:23           ` [PATCH v3 2/4] receive-pack: refactor report for proc-receive Jiang Xin
@ 2020-03-13 12:23           ` Jiang Xin
  2020-03-13 12:23           ` [PATCH v3 4/4] receive-pack: new config receive.procReceiveRefs Jiang Xin
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-13 12:23 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.26.0.rc1.5.gca1e965b06


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 4/4] receive-pack: new config receive.procReceiveRefs
  2020-03-10  6:03         ` Jiang Xin
                             ` (3 preceding siblings ...)
  2020-03-13 12:23           ` [PATCH v3 3/4] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-03-13 12:23           ` Jiang Xin
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-13 12:23 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt | 14 +++++++++
 builtin/receive-pack.c           | 49 ++++++++++++++++++++++++++------
 2 files changed, 54 insertions(+), 9 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..0178f2d478 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for/", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 23f1ae3795..2b796e654d 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1713,17 +1728,30 @@ static void execute_commands(struct command **orig_commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->have_special_ref = 1;
-			have_special_ref = 1;
-		} else {
-			have_normal_ref = 1;
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->have_special_ref = 1;
+				have_special_ref = 1;
+			} else {
+				have_normal_ref = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
+	} else {
+		have_normal_ref = 1;
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2187,6 +2215,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2302,5 +2332,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
-- 
2.26.0.rc1.5.gca1e965b06


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 0/5] New proc-receive hook for centralized workflow
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-03-22 13:18             ` Jiang Xin
  2020-03-25  5:19               ` Junio C Hamano
  2020-03-22 13:18             ` [PATCH v4 1/5] transport: not report a non-head push as a branch Jiang Xin
                               ` (4 subsequent siblings)
  5 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-03-22 13:18 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Changes since v2 and v3:

* Implement a on-the-wire protocol, and add new test helper to test
  the on-the-wire protocol. See patch 2/5.

* Refactor report() on both server and client side for reporting of
  proc-receive. See patch 5/5.


Jiang Xin (5):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  receive-pack: refactor report for proc-receive

 Documentation/config/receive.txt |  14 +
 Makefile                         |   1 +
 builtin/receive-pack.c           | 323 ++++++++++-
 refs.c                           |  11 +-
 refs.h                           |   1 +
 t/helper/test-proc-receive.c     | 164 ++++++
 t/helper/test-tool.c             |   1 +
 t/helper/test-tool.h             |   1 +
 t/t5411-proc-receive-hook.sh     | 954 +++++++++++++++++++++++++++++++
 transport.c                      |  28 +-
 10 files changed, 1478 insertions(+), 20 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh

-- 
2.26.0.rc1.33.g4914ba4bcf


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v4 1/5] transport: not report a non-head push as a branch
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
  2020-03-22 13:18             ` [PATCH v4 0/5] " Jiang Xin
@ 2020-03-22 13:18             ` Jiang Xin
  2020-03-25  6:04               ` Junio C Hamano
  2020-03-22 13:18             ` [PATCH v4 2/5] receive-pack: add new proc-receive hook Jiang Xin
                               ` (3 subsequent siblings)
  5 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-03-22 13:18 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh | 144 +++++++++++++++++++++++++++++++++++
 transport.c                  |   3 +-
 2 files changed, 146 insertions(+), 1 deletion(-)
 create mode 100755 t/t5411-proc-receive-hook.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..fe2861f2e6
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,144 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Avoid calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} 2>/dev/null)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+format_git_output () {
+	sed \
+		-e "s/  *\$//g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<COMMIT-T>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/'/\"/g"
+}
+
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		git remote add origin ../upstream &&
+		git config core.abbrev 7 &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v1.0.0" v1.0.0 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(cd workbench; git rev-parse v1.0.0) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# pre-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 "pre-receive< \$old \$new \$ref\n"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# post-receive hook\n"
+
+	while read old new ref
+	do
+		printf >&2 "post-receive< \$old \$new \$ref\n"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive
+'
+
+test_expect_success "normal git-push command" '
+	(
+		cd workbench &&
+		git push -f origin \
+			refs/tags/v1.0.0 \
+			:refs/heads/next \
+			HEAD:refs/heads/master \
+			HEAD:refs/review/master/topic \
+			HEAD:refs/heads/a/b/c
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-T> refs/tags/v1.0.0
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-T> refs/tags/v1.0.0
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To ../upstream
+	 + ce858e6...1029397 HEAD -> master (forced update)
+	 - [deleted]         next
+	 * [new tag]         v1.0.0 -> v1.0.0
+	 * [new reference]   HEAD -> refs/review/master/topic
+	 * [new branch]      HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<COMMIT-T> refs/tags/v1.0.0
+	EOF
+	test_cmp expect actual
+'
+
+test_done
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..b5b7bb841e 100644
--- a/transport.c
+++ b/transport.c
@@ -501,7 +501,8 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
 			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
+			(starts_with(ref->name, "refs/heads/") ? "[new branch]" :
+			"[new reference]")),
 			ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
-- 
2.26.0.rc1.33.g4914ba4bcf


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 2/5] receive-pack: add new proc-receive hook
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
  2020-03-22 13:18             ` [PATCH v4 0/5] " Jiang Xin
  2020-03-22 13:18             ` [PATCH v4 1/5] transport: not report a non-head push as a branch Jiang Xin
@ 2020-03-22 13:18             ` Jiang Xin
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
                                 ` (3 more replies)
  2020-03-22 13:18             ` [PATCH v4 3/5] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                               ` (2 subsequent siblings)
  5 siblings, 4 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-22 13:18 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

This "proc-receive" hook reads commands, push-options (optional), and
send result using a protocol in pkt-line format.  In the following
example, The letter "S" stands for "receive-pack" and letter "H" stands
for the hook.

    S: PKT-LINE(version=1\0push-options ...)
    S: flush-pkt

    H: PKT-LINE(version=1\0push-options ...)
    H: flush-pkt

    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt

    # Optional, only if push-options is negotiated.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # OK, run this command successfully.
    H: PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    H: PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ft)

    H: ... ...
    H: flush-pkt

After receiving a command, the hook can create/update another alternate
reference.  For example, a command for a reference "refs/for/master" may
create a special reference, such as "refs/pull/123/head".  The alternate
reference can be returned from the result in an extensible format like
"<old-oid> <new-oid> <reference> <status> [<message>]".

The result will be stored in a command list, and "receive-pack" will use
the result to replace the commands that have specific `run_proc_receive`
field turned on.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 275 +++++++++++++++++-
 t/helper/test-proc-receive.c | 164 +++++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411-proc-receive-hook.sh | 528 +++++++++++++++++++++++++++++++++++
 6 files changed, 965 insertions(+), 5 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index 9804a0758b..0a08a0504e 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..0aca08eb65 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -312,7 +312,8 @@ struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:1;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +818,241 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static int read_proc_receive_result(struct packet_reader *reader,
+				    struct command **commands)
+{
+	struct command **tail = commands;
+	int code = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+			    reader->line);
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status)
+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+			    reader->line);
+		*status++ = '\0';
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2)
+			die("protocol error: proc-receive has bad status '%s' for '%s'",
+			    status, reader->line);
+
+		FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname));
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+		cmd->run_proc_receive = 1;
+
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				cmd->error_string = xstrdup(msg);
+			else
+				cmd->error_string = "failed";
+			code = 1;
+		} else if (strcmp("ok", status)) {
+			die("protocol error: proc-receive has bad status '%s' for '%s'",
+			    status, reader->line);
+		}
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command **commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *result_commands = NULL;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot to find hook 'proc-receive'");
+		return 1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1)
+		die("protocol error: unknown proc-receive version '%d'", version);
+
+	/* Send commands */
+	for (cmd = *commands; cmd; cmd = cmd->next) {
+		char *old_hex, *new_hex;
+
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+
+		old_hex = oid_to_hex(&cmd->old_oid);
+		new_hex = oid_to_hex(&cmd->new_oid);
+
+		packet_write_fmt(proc.in, "%s %s %s",
+				 old_hex, new_hex, cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_result(&reader, &result_commands);
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		die("proc-receive did not exit properly");
+
+	sigchain_pop(SIGPIPE);
+
+	/* After receiving the result from the "proc-receive" hook,
+	 * "receive-pack" will use the result to replace commands that
+	 * have specific `run_proc_receive` field.
+	 */
+	for (cmd = *commands; cmd; cmd = cmd->next)
+		if (!cmd->run_proc_receive)
+			break;
+
+	/* Merge commands with result_commands and sort */
+	if (!cmd) {
+		*commands = result_commands;
+	} else {
+		struct command *next_cmd = cmd;
+		struct command *next_result = result_commands;
+		struct command *head = NULL;
+		struct command *tail = NULL;
+
+		if (!next_result ||
+		    strcmp(next_cmd->ref_name, next_result->ref_name) < 0) {
+			head = next_cmd;
+			next_cmd = next_cmd->next;
+		} else {
+			head = next_result;
+			next_result = next_result->next;
+		}
+		tail = head;
+
+		for (;;) {
+			if (!next_cmd) {
+				tail->next = next_result;
+				break;
+			} else if (next_cmd->run_proc_receive) {
+				next_cmd = next_cmd->next;
+			} else if (!next_result) {
+				tail->next = next_cmd;
+				next_cmd = next_cmd->next;
+				tail = tail->next;
+			} else {
+				if (strcmp(next_cmd->ref_name, next_result->ref_name) < 0) {
+					tail->next = next_cmd;
+					next_cmd = next_cmd->next;
+					tail = tail->next;
+				} else {
+					tail->next = next_result;
+					next_result = next_result->next;
+					tail = tail->next;
+				}
+			}
+		}
+		*commands = head;
+	}
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1628,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1668,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1458,16 +1694,18 @@ static void execute_commands_atomic(struct command *commands,
 	strbuf_release(&err);
 }
 
-static void execute_commands(struct command *commands,
+static void execute_commands(struct command **orig_commands,
 			     const char *unpacker_error,
 			     struct shallow_info *si,
 			     const struct string_list *push_options)
 {
+	struct command *commands = *orig_commands;
 	struct check_connected_options opt = CHECK_CONNECTED_INIT;
 	struct command *cmd;
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1735,20 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = 1;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1775,19 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive) {
+		int code;
+
+		code = run_proc_receive_hook(orig_commands, push_options);
+		commands = *orig_commands;
+		if (code) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+					cmd->error_string = "fail to run proc-receive hook";
+			}
+		}
+	}
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -2019,7 +2284,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 			update_shallow_info(commands, &si, &ref);
 		}
 		use_keepalive = KEEPALIVE_ALWAYS;
-		execute_commands(commands, unpack_status, &si,
+		execute_commands(&commands, unpack_status, &si,
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..e725cf9713
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,164 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname));
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			char *old_hex, *new_hex;
+
+			old_hex = oid_to_hex(&cmd->old_oid);
+			new_hex = oid_to_hex(&cmd->new_oid);
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				old_hex, new_hex, cmd->ref_name);
+		}
+
+		if (push_options.nr > 0) {
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+		}
+
+		if (returns.nr) {
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+		}
+	}
+
+	if (returns.nr) {
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	}
+	packet_flush(1);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index c9a232d238..9bff461446 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -43,6 +43,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index c8549fd87f..71e5f1c372 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -33,6 +33,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index fe2861f2e6..d114559792 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -50,6 +50,24 @@ format_git_output () {
 		-e "s/'/\"/g"
 }
 
+# Asynchronous sideband may generate inconsistent output messages,
+# sort before comparison.
+test_sorted_cmp () {
+	if ! $GIT_TEST_CMP "$@"
+	then
+		cmd=$GIT_TEST_CMP
+		for f in "$@"
+		do
+			sort "$f" >"$f.sorted"
+			cmd="$cmd \"$f.sorted\""
+		done
+		if ! eval $cmd
+		then
+			$GIT_TEST_CMP "$@"
+		fi
+	fi
+}
+
 test_expect_success "setup" '
 	git init --bare upstream &&
 	git init workbench &&
@@ -141,4 +159,514 @@ test_expect_success "normal git-push command" '
 	test_cmp expect actual
 '
 
+test_expect_success "cleanup" '
+	(
+		cd upstream &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v1.0.0 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:next \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot to find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To ../upstream
+	 * [new branch]      HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	(
+		cd upstream &&
+		git update-ref -d refs/heads/next
+	)
+'
+
+# TODO: report for the failure of master branch is unnecessary.
+test_expect_success "no proc-receive hook, fail all for atomic push" '
+	(
+		cd workbench &&
+		test_must_fail git push --atomic origin \
+			HEAD:next \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot to find hook "proc-receive"
+	To ../upstream
+	 ! [rejected]        master (atomic push failed)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+test_expect_success "proc-receive bad protocol: unknown version" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: unknown proc-receive version "2"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+test_expect_success "proc-receive bad protocol: no report" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 ! [remote failure]  HEAD -> refs/for/master/topic (remote failed to report status)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+test_expect_success "proc-receive bad protocol: bad oid" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+test_expect_success "proc-receive bad protocol: no status" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+test_expect_success "proc-receive bad protocol: unknown status" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+test_expect_success "proc-receive bad protocol: bad status" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+test_expect_success "proc-receive: fail to update (no message)" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To ../upstream
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+test_expect_success "proc-receive: fail to update (has message)" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To ../upstream
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+test_expect_success "proc-receive: ok" '
+	(
+		cd workbench &&
+		git push origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 * [new reference]   HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "proc-receive: report unknown ref" '
+	(
+		cd workbench &&
+		test_must_fail git push origin \
+			HEAD:refs/for/a/b/c/my/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	warning: remote reported status on unknown ref: refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 ! [remote failure]  HEAD -> refs/for/a/b/c/my/topic (remote failed to report status)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "not support push options" '
+	(
+		cd workbench &&
+		test_must_fail git push \
+			-o issue=123 \
+			-o reviewer=user1 \
+			origin \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	(
+		cd upstream &&
+		git config receive.advertisePushOptions true
+	)
+'
+
+test_expect_success "push with options" '
+	(
+		cd workbench &&
+		git push \
+			-o issue=123 \
+			-o reviewer=user1 \
+			origin \
+			HEAD:refs/heads/next \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To ../upstream
+	 * [new branch]      HEAD -> next
+	 * [new reference]   HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.26.0.rc1.33.g4914ba4bcf


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 3/5] refs.c: refactor to reuse ref_is_hidden()
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
                               ` (2 preceding siblings ...)
  2020-03-22 13:18             ` [PATCH v4 2/5] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-03-22 13:18             ` Jiang Xin
  2020-03-22 13:18             ` [PATCH v4 4/5] receive-pack: new config receive.procReceiveRefs Jiang Xin
  2020-03-22 13:18             ` [PATCH v4 5/5] receive-pack: refactor report for proc-receive Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-22 13:18 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.26.0.rc1.33.g4914ba4bcf


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 4/5] receive-pack: new config receive.procReceiveRefs
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
                               ` (3 preceding siblings ...)
  2020-03-22 13:18             ` [PATCH v4 3/5] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-03-22 13:18             ` Jiang Xin
  2020-03-22 13:18             ` [PATCH v4 5/5] receive-pack: refactor report for proc-receive Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-22 13:18 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 +++
 builtin/receive-pack.c           |  43 +++++--
 t/t5411-proc-receive-hook.sh     | 204 +++++++++++++++++++++++++++++++
 3 files changed, 254 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..0178f2d478 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for/", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 0aca08eb65..fef97e6985 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1738,15 +1753,26 @@ static void execute_commands(struct command **orig_commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = 1;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = 1;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2207,6 +2233,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2322,5 +2350,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index d114559792..f9681bed34 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -168,6 +168,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd upstream &&
+		git config --add receive.procReceiveRefs refs/for/ &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 test_expect_success "no proc-receive hook, fail to push special ref" '
 	(
 		cd workbench &&
@@ -669,4 +677,200 @@ test_expect_success "push with options" '
 	test_cmp expect actual
 '
 
+test_expect_success "cleanup" '
+	(
+		cd upstream &&
+		git update-ref -d refs/heads/next
+	)
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+test_expect_success "report update of all special refs" '
+	(
+		cd workbench &&
+		git push origin \
+			HEAD:refs/for/next/topic \
+			HEAD:refs/review/a/b/c/topic \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 * [new reference]   HEAD -> refs/for/next/topic
+	 * [new reference]   HEAD -> refs/review/a/b/c/topic
+	 * [new reference]   HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+test_expect_success "report mixed refs update (head first)" '
+	(
+		cd workbench &&
+		git push origin \
+			HEAD:refs/heads/zzz \
+			HEAD:refs/for/next/topic \
+			HEAD:refs/heads/yyy \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/zzz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/zzz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
+	To ../upstream
+	 * [new branch]      HEAD -> zzz
+	 * [new reference]   HEAD -> refs/for/next/topic
+	 * [new branch]      HEAD -> yyy
+	 * [new reference]   HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/yyy
+	<COMMIT-A> refs/heads/zzz
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	(
+		cd upstream &&
+		git update-ref -d refs/heads/yyy &&
+		git update-ref -d refs/heads/zzz
+	)
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+test_expect_success "report mixed refs update (special ref first)" '
+	(
+		cd workbench &&
+		git push origin \
+			HEAD:refs/for/next/topic \
+			$B:refs/heads/zzz \
+			HEAD:refs/review/a/b/c/topic \
+			HEAD:refs/heads/yyy \
+			HEAD:refs/for/master/topic
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/heads/zzz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/heads/zzz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 * [new reference]   HEAD -> refs/for/next/topic
+	 * [new branch]      <COMMIT-B> -> zzz
+	 * [new reference]   HEAD -> refs/review/a/b/c/topic
+	 * [new branch]      HEAD -> yyy
+	 * [new reference]   HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/yyy
+	<COMMIT-B> refs/heads/zzz
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.26.0.rc1.33.g4914ba4bcf


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 5/5] receive-pack: refactor report for proc-receive
  2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
                               ` (4 preceding siblings ...)
  2020-03-22 13:18             ` [PATCH v4 4/5] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-03-22 13:18             ` Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-22 13:18 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The "proc-receive" may update one or more references, and will send its
result one by one in pkt-line format.  Each line of the result has four
fields and one optional message field, as "<old-oid> <new-oid> <ref>
<status> [<message>]".  See the following example:

    # OK, run this command successfully.
    PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference.
    PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute.
    PKT-LINE(old-oid new-oid ref ft)

The first three fields have the same foramt as a command.

The forth field has a two-letter status code.  Available status code:

* ok: The command runs successfully.  If the optional message has a
  prefix "ref:", the hook has created/updated an alternate reference
  instead.

* ng: Fail to run the command. Error message is in the optional message
  field.

* ft: Will fallthrough to receive-pack to execute.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 21 +++++++--
 t/t5411-proc-receive-hook.sh | 90 +++++++++++++++++++++++++++++++++---
 transport.c                  | 27 +++++++----
 3 files changed, 119 insertions(+), 19 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index fef97e6985..c791f562d0 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -326,6 +326,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extra_string;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     run_proc_receive:1;
@@ -883,7 +884,12 @@ static int read_proc_receive_result(struct packet_reader *reader,
 			else
 				cmd->error_string = "failed";
 			code = 1;
-		} else if (strcmp("ok", status)) {
+		} else if (!strcmp("ok", status)) {
+			cmd->extra_string = xstrdup_or_null(msg);
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			cmd->run_proc_receive = 0;
+		} else {
 			die("protocol error: proc-receive has bad status '%s' for '%s'",
 			    status, reader->line);
 		}
@@ -2190,12 +2196,17 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
+		if (!cmd->error_string) {
+			if (!cmd->extra_string)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			else
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, ' ', cmd->extra_string);
+		} else {
 			packet_buf_write(&buf, "ng %s %s\n",
 					 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index f9681bed34..917988ab57 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -691,9 +691,9 @@ test_expect_success "setup proc-receive hook" '
 	printf >&2 "# proc-receive hook\n"
 
 	test-tool proc-receive -v \
-		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
 		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
-		-r "$ZERO_OID $A refs/for/master/topic ok"
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
 	EOF
 	chmod a+x upstream/hooks/proc-receive
 '
@@ -716,17 +716,17 @@ test_expect_success "report update of all special refs" '
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To ../upstream
-	 * [new reference]   HEAD -> refs/for/next/topic
+	 * [new reference]   HEAD -> refs/pull/123/head
 	 * [new reference]   HEAD -> refs/review/a/b/c/topic
-	 * [new reference]   HEAD -> refs/for/master/topic
+	 * [new reference]   HEAD -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
 	(
@@ -873,4 +873,82 @@ test_expect_success "report mixed refs update (special ref first)" '
 	test_cmp expect actual
 '
 
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	(
+		cd upstream &&
+		git config --add receive.procReceiveRefs refs/
+	)
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$A $ZERO_OID refs/heads/yyy ft" \
+		-r "$B $A refs/heads/zzz ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$A $B refs/heads/master ft" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+test_expect_success "report test: fallthrough" '
+	(
+		cd workbench &&
+		git push -f origin \
+			:refs/heads/yyy \
+			$A:refs/heads/zzz \
+			HEAD:refs/for/master/topic \
+			HEAD:refs/for/next/topic \
+			$B:refs/heads/master
+	) >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/yyy
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/zzz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/yyy
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/zzz
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/yyy ft
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/zzz ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/master ft
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/yyy
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/zzz
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To ../upstream
+	   1029397..ce858e6  <COMMIT-B> -> master
+	 - [deleted]         yyy
+	 + ce858e6...1029397 <COMMIT-A> -> zzz (forced update)
+	 * [new reference]   HEAD -> refs/pull/123/head
+	 * [new reference]   HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	(
+		cd upstream &&
+		git show-ref
+	) >out &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/zzz
+	EOF
+	test_cmp expect actual
+'
+
 test_done
diff --git a/transport.c b/transport.c
index b5b7bb841e..d51af11d48 100644
--- a/transport.c
+++ b/transport.c
@@ -463,11 +463,16 @@ static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	char *to_name = to->name;
+
+	if (to->remote_status && !strncmp("ref:", to->remote_status, 4))
+		to_name = to->remote_status + 4;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +486,9 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -498,13 +503,19 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
 				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+	else if (is_null_oid(&ref->old_oid)) {
+		char *refname;
+
+		if (ref->remote_status && !strncmp(ref->remote_status, "ref:", 4))
+			refname = ref->remote_status + 4;
+		else
+			refname = ref->name;
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			(starts_with(ref->name, "refs/heads/") ? "[new branch]" :
+			(starts_with(refname, "refs/tags/") ? "[new tag]" :
+			(starts_with(refname, "refs/heads/") ? "[new branch]" :
 			"[new reference]")),
 			ref, ref->peer_ref, NULL, porcelain, summary_width);
-	else {
+	} else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
-- 
2.26.0.rc1.33.g4914ba4bcf


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v4 0/5] New proc-receive hook for centralized workflow
  2020-03-22 13:18             ` [PATCH v4 0/5] " Jiang Xin
@ 2020-03-25  5:19               ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-03-25  5:19 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List

Jiang Xin <worldhello.net@gmail.com> writes:

> Changes since v2 and v3:
>
> * Implement a on-the-wire protocol, and add new test helper to test
>   the on-the-wire protocol. See patch 2/5.
>
> * Refactor report() on both server and client side for reporting of
>   proc-receive. See patch 5/5.
>
>
> Jiang Xin (5):
>   transport: not report a non-head push as a branch
>   receive-pack: add new proc-receive hook
>   refs.c: refactor to reuse ref_is_hidden()
>   receive-pack: new config receive.procReceiveRefs
>   receive-pack: refactor report for proc-receive

t5516 detects breakage of these patches that are queued directly on
top of 2.26.

The tip of 'pu' tonight includes this, and fails.

E.g. https://travis-ci.org/github/git/git/builds/666579791

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v4 1/5] transport: not report a non-head push as a branch
  2020-03-22 13:18             ` [PATCH v4 1/5] transport: not report a non-head push as a branch Jiang Xin
@ 2020-03-25  6:04               ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-03-25  6:04 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> +# NOTE: Avoid calling this function from a subshell since variable
> +# assignments will disappear when subshell exits.

s/Avoid/Never/; the test_tick helper used in this function has the
same issue.

> +create_commits_in () {
> +	repo="$1" &&
> +	if ! parent=$(git -C "$repo" rev-parse HEAD^{} 2>/dev/null)

Do we need to discard the standard error stream like this?  As long
as this helper function is called inside a test_expect_thing, the
error will not be seen and when debugging the test, we would want to
see a failure (which indicates that we are creating a root commit).

> +format_git_output () {

I suspect this is to make output from _some_ "git" subcommand into a
symbolic form so that exact object names would not have to be used
in comparison, but this obviously cannot take _any_ git subcommand,
but a specific one.  The name does not say which one, which is
disservice to readers.

> +	sed \
> +		-e "s/  *\$//g" \

What's the point of /g?  You are anchoring the pattern (i.e. one or
more SP) to the right end of the line, so it's not like it would
transform "a  b c   " into "abc".  Also it would be sufficient to
say "zero or more" and that would be shorter, I think, i.e.

		-e 's/ *$//' \

> +		-e "s/$A/<COMMIT-A>/g" \
> +		-e "s/$B/<COMMIT-B>/g" \
> +		-e "s/$TAG/<COMMIT-T>/g" \

A and B are commits, so the symbolic <COMMIT-A> and <COMMIT-B> do
make sense, but wouldn't TAG be an annotated tag?  Wouldn't it be
<TAG-A> perhaps?

> +		-e "s/$ZERO_OID/<ZERO-OID>/g" \

Maekes sense.

> +		-e "s/'/\"/g"

I am not sure what is going on here.  Why turn <don't> into <don"t>?
Without exactly knowing what is getting munged, a reader cannot tell
what is going on here.  Let's read on to figure it out.

In any case, I think most of this helper is not about "formatting"
output, but hiding, getting rid of, redacting or censoring the
details for easier comparison.  I'd prefer to see some different
verb to be used for its name.

> +}
> +
> +test_expect_success "setup" '
> +	git init --bare upstream &&
> +	git init workbench &&
> +	create_commits_in workbench A B &&
> +	(
> +		cd workbench &&
> +		git remote add origin ../upstream &&
> +		git config core.abbrev 7 &&

This '7' is "use at least seven hexdigits"; is that really what we
want?  Depending on chance, some objects may be shown using 8 or
more---is our "munge output into symbolic form for comparison"
script prepared for such a case?

> +		git update-ref refs/heads/master $A &&
> +		git tag -m "v1.0.0" v1.0.0 $A &&
> +		git push origin \
> +			$B:refs/heads/master \
> +			$A:refs/heads/next
> +	) &&
> +	TAG=$(cd workbench; git rev-parse v1.0.0) &&

Why not "git -C workbench rev-parse v1.0.0"?

So, at this point, there are two repositories, upstream and
workbench, and two commits A and B (B is newer).  workbench has A at
the tip of its 'master'; upstream has A at the tip of 'next' and B
(which is newer than A) at the tip of 'master'.

> +
> +	# setup pre-receive hook
> +	cat >upstream/hooks/pre-receive <<-EOF &&

Wouldn't it make it easier to read the resulting text if you quoted
the end-of-here-text marker here, i.e. "<<\-EOF"?  That way, you can
lose backslash before $old, $new and $ref.

> +	#!/bin/sh
> +
> +	printf >&2 "# pre-receive hook\n"
> +
> +	while read old new ref
> +	do
> +		printf >&2 "pre-receive< \$old \$new \$ref\n"
> +	done
> +	EOF
> +	# setup post-receive hook
> +	cat >upstream/hooks/post-receive <<-EOF &&

Likewise.

> +test_expect_success "normal git-push command" '
> +	(
> +		cd workbench &&
> +		git push -f origin \
> +			refs/tags/v1.0.0 \
> +			:refs/heads/next \
> +			HEAD:refs/heads/master \
> +			HEAD:refs/review/master/topic \
> +			HEAD:refs/heads/a/b/c
> +	) >out 2>&1 &&

Do we need to worry about progress output (which we would want to
squelch, I presume, for the purpose of comparing with the
"expectation")?  Would it be just the matter of giving --no-progress?

> +	format_git_output <out >actual &&
> +	cat >expect <<-EOF &&
> +	remote: # pre-receive hook
> +	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
> +	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
> +	remote: pre-receive< <ZERO-OID> <COMMIT-T> refs/tags/v1.0.0
> +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
> +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c

Do we guarantee the order in which these received refs are reported,
or do we somehow need to sort (presumably inside the hook)?  The
same question applies to the post-receive side, too.

> +	remote: # post-receive hook
> +	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
> +	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
> +	remote: post-receive< <ZERO-OID> <COMMIT-T> refs/tags/v1.0.0
> +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
> +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
> +	To ../upstream
> +	 + ce858e6...1029397 HEAD -> master (forced update)
> +	 - [deleted]         next
> +	 * [new tag]         v1.0.0 -> v1.0.0
> +	 * [new reference]   HEAD -> refs/review/master/topic
> +	 * [new branch]      HEAD -> a/b/c
> +	EOF
> +	test_cmp expect actual &&
> +	(
> +		cd upstream &&
> +		git show-ref

This one I know we give output in a reliable order, but I do not
offhand know if we give any written guarantee.  Perhaps we should
document it if we haven't already (i.e. it is OK the "expected"
output below assumes that the output is sorted by full refnames in
ASCII order).

> +	) >out &&
> +	format_git_output <out >actual &&
> +	cat >expect <<-EOF &&
> +	<COMMIT-A> refs/heads/a/b/c
> +	<COMMIT-A> refs/heads/master
> +	<COMMIT-A> refs/review/master/topic
> +	<COMMIT-T> refs/tags/v1.0.0
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_done
> diff --git a/transport.c b/transport.c
> index 1fdc7dac1a..b5b7bb841e 100644
> --- a/transport.c
> +++ b/transport.c
> @@ -501,7 +501,8 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
>  	else if (is_null_oid(&ref->old_oid))
>  		print_ref_status('*',
>  			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
> -			"[new branch]"),
> +			(starts_with(ref->name, "refs/heads/") ? "[new branch]" :
> +			"[new reference]")),
>  			ref, ref->peer_ref, NULL, porcelain, summary_width);

The original is largely to blame, but I couldn't read the above and
understand what the above is doing, before reformatting it like so:

	else if (is_null_oid(&ref->old_oid))
		print_ref_status('*',
				 (starts_with(ref->name, "refs/tags/")
				  ? "[new tag]"
				  : (starts_with(ref->name, "refs/heads/")
				     ? "[new branch]"
				     : "[new reference]")),
				 ref, ref->peer_ref, NULL, porcelain, summary_width);

When long subexpressions are involved, ternary operator ?: is easier
to read, especially when nested, if you can see its parse tree when
you tilt your head 90-degrees sideways (i.e. the same direction as
you can see a smile in ;-).

Thanks.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH 0/3] Never report references we not push
  2020-03-22 13:18             ` [PATCH v4 2/5] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-03-25 14:36               ` Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
                                   ` (4 more replies)
  2020-03-25 14:36               ` [PATCH 1/3] t5543: never report what we do not push Jiang Xin
                                 ` (2 subsequent siblings)
  3 siblings, 5 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-25 14:36 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Emily Shaffer; +Cc: Jiang Xin

A strange behavior is found when writing test cases for topic
'jx/proc-receive-hook':

> # TODO: report for the failure of master branch is unnecessary.
> test_expect_success "no proc-receive hook, fail all for atomic push" '
> 	(
> 		cd workbench &&
> 		test_must_fail git push --atomic origin \
> 			HEAD:next \
> 			HEAD:refs/for/master/topic
> 	) >out 2>&1 &&
> 	format_git_output <out >actual &&
> 	cat >expect <<-EOF &&
> 	remote: # pre-receive hook
> 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
> 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
> 	remote: error: cannot to find hook "proc-receive"
> 	To ../upstream
> 	 ! [rejected]        master (atomic push failed)

We do not push "master" branch, but git reports status for it.

> 	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
> 	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
> 	error: failed to push some refs to "../upstream"
> 	EOF
> 	test_cmp expect actual &&
> 	(
> 		cd upstream &&
> 		git show-ref
> 	) >out &&
> 	format_git_output <out >actual &&
> 	cat >expect <<-EOF &&
> 	<COMMIT-A> refs/heads/master
> 	EOF
> 	test_cmp expect actual
> '

This issue is introduced by commit v2.22.0-1-g3bca1e7f9f (transport-helper:
enforce atomic in push_refs_with_push, 2019-07-11).  That commit wanted to
fix report issue of HTTP protocol, but marked all remote references failure
for atomic push.

--

Jiang Xin (3):
  t5543: never report what we do not push
  send-pack: mark failure of atomic push properly
  transport-helper: enforce atomic in push_refs_with_push

 send-pack.c                |  2 +
 t/t5541-http-push-smart.sh | 12 ++++-
 t/t5543-atomic-push.sh     | 92 ++++++++++++++++++++++++++++++++++++++
 transport-helper.c         | 15 +++++++
 transport.c                | 14 ------
 5 files changed, 119 insertions(+), 16 deletions(-)

-- 
2.26.0.3.ga7a9d752d4


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH 1/3] t5543: never report what we do not push
  2020-03-22 13:18             ` [PATCH v4 2/5] receive-pack: add new proc-receive hook Jiang Xin
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
@ 2020-03-25 14:36               ` Jiang Xin
  2020-03-25 15:05                 ` Junio C Hamano
  2020-03-25 14:36               ` [PATCH 2/3] send-pack: mark failure of atomic push properly Jiang Xin
  2020-03-25 14:36               ` [PATCH 3/3] transport-helper: enforce atomic in push_refs_with_push Jiang Xin
  3 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-03-25 14:36 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Emily Shaffer; +Cc: Jiang Xin

When we push some references to the git server, we expect git to report
the status of the references we are pushing; no more, no less.  But when
pusing with atomic mode, if some references cannot be pushed, Git reports
the reject message on all references in the remote repository.

Add new test cases in t5543, and fix them in latter commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5543-atomic-push.sh | 92 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 92 insertions(+)

diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 7079bcf9a0..4b4c0a262b 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -27,6 +27,13 @@ test_refs () {
 	test_cmp expect actual
 }
 
+format_git_output () {
+	awk '/^(To| !) / {print}' | \
+	sed \
+		-e "s/  *\$//g" \
+		-e "s/'/\"/g"
+}
+
 test_expect_success 'atomic push works for a single branch' '
 	mk_repo_pair &&
 	(
@@ -191,4 +198,89 @@ test_expect_success 'atomic push is not advertised if configured' '
 	test_refs master HEAD@{1}
 '
 
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+# Atomic push            : master(2)               two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by update hook)' '
+	mk_repo_pair &&
+	(
+		cd workbench &&
+		# Keep constant output.
+		git config core.abbrev 7 &&
+		test_commit one &&
+		git branch foo &&
+		git push up master one foo &&
+		git tag -d one
+	) &&
+	(
+		mkdir -p upstream/.git/hooks &&
+		cat >upstream/.git/hooks/update <<-EOF &&
+		#!/bin/sh
+
+		if test "\$1" = "refs/heads/bar"
+		then
+			echo >2 "Pusing to branch bar is prohibited"
+			exit 1
+		fi
+		EOF
+		chmod a+x upstream/.git/hooks/update
+	) &&
+	(
+		cd workbench &&
+		test_commit two &&
+		git branch bar
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master two bar >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] two -> two (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+	(
+		cd workbench &&
+		git remote remove up &&
+		git remote add up ../upstream
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic --mirror up >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] one (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	 ! [remote rejected] two -> two (atomic push failure)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(2) one(1) foo(1)
+# References in workbench: master(1)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by non-ff)' '
+	rm upstream/.git/hooks/update &&
+	(
+		cd workbench &&
+		git push up master &&
+		git reset --hard HEAD^
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master foo bar >out 2>&1 &&
+	format_git_output <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [rejected]        master -> master (non-fast-forward)
+	 ! [rejected]        bar -> bar (atomic push failed)
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.26.0.3.ga7a9d752d4


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 2/3] send-pack: mark failure of atomic push properly
  2020-03-22 13:18             ` [PATCH v4 2/5] receive-pack: add new proc-receive hook Jiang Xin
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
  2020-03-25 14:36               ` [PATCH 1/3] t5543: never report what we do not push Jiang Xin
@ 2020-03-25 14:36               ` Jiang Xin
  2020-03-25 15:15                 ` Junio C Hamano
  2020-03-25 14:36               ` [PATCH 3/3] transport-helper: enforce atomic in push_refs_with_push Jiang Xin
  3 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-03-25 14:36 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Emily Shaffer; +Cc: Jiang Xin

When pusing with SSH or other smart protocol, references are validated
by function `check_to_send_update()` beforce they are sent in commands
to `send_pack()` of "receve-pack".  For atomic push, if a reference is
rejected after the validation, only references pushed by user should be
marked as failure, instead of report failure on all remote references.

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) wanted to fix report issue of HTTP
protocol, but marked all remote references failure for atomic push.

Revert part of that commit and add additional status for function
`atomic_push_failure()`.  The additional status for it except the
"REF_STATUS_EXPECTING_REPORT" status are:

- REF_STATUS_NONE : Not marked as "REF_STATUS_EXPECTING_REPORT" yet.
- REF_STATUS_OK   : Assume OK for dryrun or status_report is disabled.

This commit break test case in t5541, and will fix in other commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c                |  2 ++
 t/t5541-http-push-smart.sh |  2 +-
 t/t5543-atomic-push.sh     |  6 +++---
 transport.c                | 14 --------------
 4 files changed, 6 insertions(+), 18 deletions(-)

diff --git a/send-pack.c b/send-pack.c
index 0407841ae8..ff016c468c 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -334,6 +334,8 @@ static int atomic_push_failure(struct send_pack_args *args,
 			continue;
 
 		switch (ref->status) {
+		case REF_STATUS_NONE:
+		case REF_STATUS_OK:
 		case REF_STATUS_EXPECTING_REPORT:
 			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
 			continue;
diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 23be8ce92d..2c2c3fb0f5 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,7 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
+test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 4b4c0a262b..d7be1c098b 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -201,7 +201,7 @@ test_expect_success 'atomic push is not advertised if configured' '
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
 # Atomic push            : master(2)               two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by update hook)' '
+test_expect_success 'atomic push reports (reject by update hook)' '
 	mk_repo_pair &&
 	(
 		cd workbench &&
@@ -244,7 +244,7 @@ test_expect_failure 'atomic push reports (reject by update hook)' '
 
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+test_expect_success 'atomic push reports (mirror, but reject by update hook)' '
 	(
 		cd workbench &&
 		git remote remove up &&
@@ -265,7 +265,7 @@ test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
 
 # References in upstream : master(2) one(1) foo(1)
 # References in workbench: master(1)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by non-ff)' '
+test_expect_success 'atomic push reports (reject by non-ff)' '
 	rm upstream/.git/hooks/update &&
 	(
 		cd workbench &&
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..75c5c9fe98 100644
--- a/transport.c
+++ b/transport.c
@@ -1240,20 +1240,6 @@ int transport_push(struct repository *r,
 		err = push_had_errors(remote_refs);
 		ret = push_ret | err;
 
-		if ((flags & TRANSPORT_PUSH_ATOMIC) && err) {
-			struct ref *it;
-			for (it = remote_refs; it; it = it->next)
-				switch (it->status) {
-				case REF_STATUS_NONE:
-				case REF_STATUS_UPTODATE:
-				case REF_STATUS_OK:
-					it->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-					break;
-				default:
-					break;
-				}
-		}
-
 		if (!quiet || err)
 			transport_print_push_status(transport->url, remote_refs,
 					verbose | porcelain, porcelain,
-- 
2.26.0.3.ga7a9d752d4


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 3/3] transport-helper: enforce atomic in push_refs_with_push
  2020-03-22 13:18             ` [PATCH v4 2/5] receive-pack: add new proc-receive hook Jiang Xin
                                 ` (2 preceding siblings ...)
  2020-03-25 14:36               ` [PATCH 2/3] send-pack: mark failure of atomic push properly Jiang Xin
@ 2020-03-25 14:36               ` Jiang Xin
  2020-03-25 15:32                 ` Junio C Hamano
  3 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-03-25 14:36 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Emily Shaffer; +Cc: Jiang Xin

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) noticed the incomplete report of
failure of an atomic push for HTTP protocol.  But the implementation
has a flaw that mark all remote references as failure.

Only mark necessary references as failure in `push_refs_with_push()` of
transport-helper.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5541-http-push-smart.sh | 14 +++++++++++---
 transport-helper.c         | 15 +++++++++++++++
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 2c2c3fb0f5..afc680d5e3 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,10 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
+## References of remote: atomic1(1)            master(2) collateral(2) other(2)
+## References of local :            atomic2(2) master(1) collateral(3) other(2) collateral1(3) atomic(1)
+## Atomic push         :                       master(1) collateral(3)                         atomic(1)
+test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
@@ -189,7 +192,8 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 	test_commit atomic2 &&
 	git branch collateral &&
 	git branch other &&
-	git push "$up" master collateral other &&
+	git push "$up" atomic1 master collateral other &&
+	git tag -d atomic1 &&
 
 	# collateral is a valid push, but should be failed by atomic push
 	git checkout collateral &&
@@ -224,7 +228,11 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 
 	# the collateral failure refs should be indicated to the user
 	grep "^ ! .*rejected.* atomic -> atomic .*atomic push failed" output &&
-	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output
+	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output &&
+
+	# never report what we do not push
+	! grep "^ ! .*rejected.* atomic1 " output &&
+	! grep "^ ! .*rejected.* other " output
 '
 
 test_expect_success 'push --atomic fails on server-side errors' '
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..ab3b52eb14 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -894,6 +894,21 @@ static int push_refs_with_push(struct transport *transport,
 		case REF_STATUS_REJECT_STALE:
 		case REF_STATUS_REJECT_ALREADY_EXISTS:
 			if (atomic) {
+				/* Mark other refs as failed */
+				for (ref = remote_refs; ref; ref = ref->next) {
+					if (!ref->peer_ref && !mirror)
+						continue;
+
+					switch (ref->status) {
+					case REF_STATUS_NONE:
+					case REF_STATUS_OK:
+					case REF_STATUS_EXPECTING_REPORT:
+						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+						continue;
+					default:
+						break; /* do nothing */
+					}
+				}
 				string_list_clear(&cas_options, 0);
 				return 0;
 			} else
-- 
2.26.0.3.ga7a9d752d4


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH 1/3] t5543: never report what we do not push
  2020-03-25 14:36               ` [PATCH 1/3] t5543: never report what we do not push Jiang Xin
@ 2020-03-25 15:05                 ` Junio C Hamano
  2020-03-26  2:25                   ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-03-25 15:05 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Emily Shaffer, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> +format_git_output () {

Unless this helper is able to take any git output and massage,
please describe what kind of git output it is meant to handle.

Also, "format" does not tell anything to the readers why it wants to
transform its input or what its output is supposed to look like.  It
does not help readers and future developers.

> +	awk '/^(To| !) / {print}' | \
> +	sed \
> +		-e "s/  *\$//g" \

What's the point of /g?  You are anchoring the pattern (i.e. one or
more SP) to the right end of the line, so it's not like it would
transform "a  b c   " into "abc".  Also it would be sufficient to
say "zero or more" and that would be shorter, I think.

> +		-e "s/'/\"/g"

It is unclear what this thing is for.  If the output from a git
subcommand you are munging with the helper says <don't>, this will
turn it into <don"t>, presumably before comparing it with the
expected output you'd literally prepare in the test script.  Are the
git subcommands whose output you are munging unstable and sometimes
use single and sometimes use double quotes?  

If not, if you used single quotes when preparing the expected
output, wouldn't you be able to do without this?

Is it because you'd have the code that prepares the expected output
inside a sq pair (because it is in test_expect_thing), and it is
cumbersome to write a literal single quote?  If that is the reason,
that is understandable, but I think readers deserve some comments
explaining why these transformations are done.  Please do not waste
readers' time.

It looks wasteful to pipe awk output to sed.  I wonder if something
along the lines of

	sed -ne "/^\(To\| !\) /{
		s/ *\$//
		s/'/\"/g
		p
	}"

would do the same thing with a single tool.

> +# References in upstream : master(1) one(1) foo(1)
> +# References in workbench: master(2)        foo(1) two(2) bar(2)
> +# Atomic push            : master(2)               two(2) bar(2)
> +test_expect_failure 'atomic push reports (reject by update hook)' '
> +	mk_repo_pair &&
> +	(
> +		cd workbench &&
> +		# Keep constant output.
> +		git config core.abbrev 7 &&

This '7' is "use at least seven hexdigits", so it does not give any
guarantee that your output will be stable.  Depending on chance,
some objects may be shown using 8 or more---is our "munge output
before comparison" helper prepared for such a case?

> +		test_commit one &&
> +		git branch foo &&
> +		git push up master one foo &&
> +		git tag -d one
> +	) &&
> +	(
> +		mkdir -p upstream/.git/hooks &&
> +		cat >upstream/.git/hooks/update <<-EOF &&
> +		#!/bin/sh
> +
> +		if test "\$1" = "refs/heads/bar"
> +		then
> +			echo >2 "Pusing to branch bar is prohibited"

Meant to redirect to the standard error stream, not a file "2"?

> +			exit 1
> +		fi
> +		EOF
> +		chmod a+x upstream/.git/hooks/update
> +	) &&
> +	(
> +		cd workbench &&
> +		test_commit two &&
> +		git branch bar
> +	) &&

At this point, we have the refs described in the comment at the
beginning (which is greatly appreciated---it hels understanding what
is going on).

> +	test_must_fail git -C workbench \
> +		push --atomic up master two bar >out 2>&1 &&

As "git push" does not auto-follow tags, what we push will be
exactly these three refs.

> +	format_git_output <out >actual &&
> +	cat >expect <<-EOF &&
> +	To ../upstream
> +	 ! [remote rejected] master -> master (atomic push failure)
> +	 ! [remote rejected] two -> two (atomic push failure)
> +	 ! [remote rejected] bar -> bar (hook declined)
> +	EOF

And we expect them to be rejected, as 'bar' needs to stay constant
there.  

> +	test_cmp expect actual

All makes sense.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH 2/3] send-pack: mark failure of atomic push properly
  2020-03-25 14:36               ` [PATCH 2/3] send-pack: mark failure of atomic push properly Jiang Xin
@ 2020-03-25 15:15                 ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-03-25 15:15 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Emily Shaffer, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> When pusing with SSH or other smart protocol, references are validated
> by function `check_to_send_update()` beforce they are sent in commands

beforce -> before

> to `send_pack()` of "receve-pack".  For atomic push, if a reference is
> rejected after the validation, only references pushed by user should be
> marked as failure, instead of report failure on all remote references.
>
> Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
> push_refs_with_push, 2019-07-11) wanted to fix report issue of HTTP
> protocol, but marked all remote references failure for atomic push.
>
> Revert part of that commit and add additional status for function
> `atomic_push_failure()`.  The additional status for it except the
> "REF_STATUS_EXPECTING_REPORT" status are:
>
> - REF_STATUS_NONE : Not marked as "REF_STATUS_EXPECTING_REPORT" yet.
> - REF_STATUS_OK   : Assume OK for dryrun or status_report is disabled.
>
> This commit break test case in t5541, and will fix in other commit.

Presumably the fix in 3/3 is too big to be included in here?  Let's
see how it goes, as such a strategy sometimes helps understand the
change and sometimes does not.

> diff --git a/send-pack.c b/send-pack.c
> index 0407841ae8..ff016c468c 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -334,6 +334,8 @@ static int atomic_push_failure(struct send_pack_args *args,
>  			continue;
>  
>  		switch (ref->status) {
> +		case REF_STATUS_NONE:
> +		case REF_STATUS_OK:
>  		case REF_STATUS_EXPECTING_REPORT:
>  			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
>  			continue;

This is fed the whole *remote_refs list, iterates over them but we
only come here if the ref is actually pushed (i.e. peer_ref is set,
or we are doing a mirror push), so it does sound like the right
place to do this.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH 3/3] transport-helper: enforce atomic in push_refs_with_push
  2020-03-25 14:36               ` [PATCH 3/3] transport-helper: enforce atomic in push_refs_with_push Jiang Xin
@ 2020-03-25 15:32                 ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-03-25 15:32 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Emily Shaffer, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/transport-helper.c b/transport-helper.c
> index 20a7185ec4..ab3b52eb14 100644
> --- a/transport-helper.c
> +++ b/transport-helper.c
> @@ -894,6 +894,21 @@ static int push_refs_with_push(struct transport *transport,
>  		case REF_STATUS_REJECT_STALE:
>  		case REF_STATUS_REJECT_ALREADY_EXISTS:
>  			if (atomic) {
> +				/* Mark other refs as failed */
> +				for (ref = remote_refs; ref; ref = ref->next) {
> +					if (!ref->peer_ref && !mirror)
> +						continue;
> +
> +					switch (ref->status) {
> +					case REF_STATUS_NONE:
> +					case REF_STATUS_OK:
> +					case REF_STATUS_EXPECTING_REPORT:
> +						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
> +						continue;
> +					default:
> +						break; /* do nothing */
> +					}
> +				}

OK, so this is more in line with the check done in send_pack() that
fails the push before we even send any pack data.  I wonder if it is
worth considering to move the logic of this loop into a helper
function so that this and the other one in 2/3 can call it and stay
in sync, something along the lines of

	void reject_push_to_other_refs(struct ref *ref, int mirror_mode)
	{
		for (; ref; ref = ref->next) {
			... one iteration of the above loop ...
		}
	}

Then the above part would become

		case REF_STATUS_REJECT_FOO:
			if (atomic)
				reject_push_to_other_refs(remote_refs, mirror);

and the part modified by 2/3 would also become

	static int atomic_push_failure(...)
	{
		reject_push_to_other_refs(remote_refs, args->send_mirror);
		return error("atomic push failed ...");
	}

I am not sure if it is better to keep 2/3 and 3/3 separate or make
them into a single step, but perhaps it is because I am not getting
the true reason why you made them separate in the first place.

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH 1/3] t5543: never report what we do not push
  2020-03-25 15:05                 ` Junio C Hamano
@ 2020-03-26  2:25                   ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-26  2:25 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Emily Shaffer, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年3月25日周三 下午11:05写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > +format_git_output () {
>
> Unless this helper is able to take any git output and massage,
> please describe what kind of git output it is meant to handle.
>
Wil use "cut_status_report_and_tidy" as the function name.

> Also, "format" does not tell anything to the readers why it wants to
> transform its input or what its output is supposed to look like.  It
> does not help readers and future developers.
>
> > +     awk '/^(To| !) / {print}' | \
> > +     sed \
> > +             -e "s/  *\$//g" \
>
> What's the point of /g?  You are anchoring the pattern (i.e. one or
> more SP) to the right end of the line, so it's not like it would
> transform "a  b c   " into "abc".  Also it would be sufficient to
> say "zero or more" and that would be shorter, I think.

I want to remove the trailing spaces in git output.  I don't know why
there are trailing spaces in each "remote: " message.
But there is not trailing spaces in status report message, so I will
remote it in next reroll.

>
> > +             -e "s/'/\"/g"
>
> It is unclear what this thing is for.  If the output from a git
> subcommand you are munging with the helper says <don't>, this will
> turn it into <don"t>, presumably before comparing it with the
> expected output you'd literally prepare in the test script.  Are the
> git subcommands whose output you are munging unstable and sometimes
> use single and sometimes use double quotes?
>
> If not, if you used single quotes when preparing the expected
> output, wouldn't you be able to do without this?
>
> Is it because you'd have the code that prepares the expected output
> inside a sq pair (because it is in test_expect_thing), and it is
> cumbersome to write a literal single quote?  If that is the reason,
> that is understandable, but I think readers deserve some comments
> explaining why these transformations are done.  Please do not waste
> readers' time.

To prepare expect message, sometime I have to escape the single quote like this

    cat >expect <<-EOF
        error: failed to push some refs to '"'"'../upstream'"'"'
    EOF

It's boring to type '"'"' instead of a double quote.

>
> It looks wasteful to pipe awk output to sed.  I wonder if something
> along the lines of
>
>         sed -ne "/^\(To\| !\) /{
>                 s/ *\$//
>                 s/'/\"/g
>                 p
>         }"
>
> would do the same thing with a single tool.

That's better.

> > +# References in upstream : master(1) one(1) foo(1)
> > +# References in workbench: master(2)        foo(1) two(2) bar(2)
> > +# Atomic push            : master(2)               two(2) bar(2)
> > +test_expect_failure 'atomic push reports (reject by update hook)' '
> > +     mk_repo_pair &&
> > +     (
> > +             cd workbench &&
> > +             # Keep constant output.
> > +             git config core.abbrev 7 &&
>
> This '7' is "use at least seven hexdigits", so it does not give any
> guarantee that your output will be stable.  Depending on chance,
> some objects may be shown using 8 or more---is our "munge output
> before comparison" helper prepared for such a case?

Will use sed -e 's/   */ /g' on output message to make it stable.

>
> > +             test_commit one &&
> > +             git branch foo &&
> > +             git push up master one foo &&
> > +             git tag -d one
> > +     ) &&
> > +     (
> > +             mkdir -p upstream/.git/hooks &&
> > +             cat >upstream/.git/hooks/update <<-EOF &&
> > +             #!/bin/sh
> > +
> > +             if test "\$1" = "refs/heads/bar"
> > +             then
> > +                     echo >2 "Pusing to branch bar is prohibited"
>
> Meant to redirect to the standard error stream, not a file "2"?

It's a typo, should be "echo >&2".

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v2 0/4] Never report references we not push
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
@ 2020-03-29 14:33                 ` Jiang Xin
  2020-03-29 14:35                   ` Jiang Xin
                                     ` (6 more replies)
  2020-03-29 14:33                 ` [PATCH v2 1/4] t5543: never report what we do not push Jiang Xin
                                   ` (3 subsequent siblings)
  4 siblings, 7 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-29 14:33 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Hi,

Patches 2/4, 4/3 and 4/4 can be squashed into one commit.

## Changes since v1:

* Add a new common method `reject_atomic_push()`, which is reused for 
  atomic push rejection for SSH and HTTP protocol.

* Other changes see the attached range diff.


## Range diff v1..v2

1:  e1c7aa3501 ! 1:  d84ebf30b5 t5543: never report what we do not push
    @@ t/t5543-atomic-push.sh: test_refs () {
      	test_cmp expect actual
      }
      
    -+format_git_output () {
    -+	awk '/^(To| !) / {print}' | \
    -+	sed \
    -+		-e "s/  *\$//g" \
    -+		-e "s/'/\"/g"
    ++fmt_status_report () {
    ++	sed -n \
    ++		-e "/^To / { s/   */ /g; p; }" \
    ++		-e "/^ ! / { s/   */ /g; p; }"
     +}
     +
      test_expect_success 'atomic push works for a single branch' '
    @@ t/t5543-atomic-push.sh: test_expect_success 'atomic push is not advertised if co
     +	mk_repo_pair &&
     +	(
     +		cd workbench &&
    -+		# Keep constant output.
    -+		git config core.abbrev 7 &&
     +		test_commit one &&
     +		git branch foo &&
     +		git push up master one foo &&
    @@ t/t5543-atomic-push.sh: test_expect_success 'atomic push is not advertised if co
     +
     +		if test "\$1" = "refs/heads/bar"
     +		then
    -+			echo >2 "Pusing to branch bar is prohibited"
    ++			echo >&2 "Pusing to branch bar is prohibited"
     +			exit 1
     +		fi
     +		EOF
    @@ t/t5543-atomic-push.sh: test_expect_success 'atomic push is not advertised if co
     +	) &&
     +	test_must_fail git -C workbench \
     +		push --atomic up master two bar >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	fmt_status_report <out >actual &&
     +	cat >expect <<-EOF &&
     +	To ../upstream
     +	 ! [remote rejected] master -> master (atomic push failure)
    @@ t/t5543-atomic-push.sh: test_expect_success 'atomic push is not advertised if co
     +	) &&
     +	test_must_fail git -C workbench \
     +		push --atomic --mirror up >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	fmt_status_report <out >actual &&
     +	cat >expect <<-EOF &&
     +	To ../upstream
     +	 ! [remote rejected] master -> master (atomic push failure)
    @@ t/t5543-atomic-push.sh: test_expect_success 'atomic push is not advertised if co
     +	) &&
     +	test_must_fail git -C workbench \
     +		push --atomic up master foo bar >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	fmt_status_report <out >actual &&
     +	cat >expect <<-EOF &&
     +	To ../upstream
    -+	 ! [rejected]        master -> master (non-fast-forward)
    -+	 ! [rejected]        bar -> bar (atomic push failed)
    ++	 ! [rejected] master -> master (non-fast-forward)
    ++	 ! [rejected] bar -> bar (atomic push failed)
     +	EOF
     +	test_cmp expect actual
     +'
2:  50f1e02a2f ! 2:  a76340633b send-pack: mark failure of atomic push properly
    @@ Metadata
      ## Commit message ##
         send-pack: mark failure of atomic push properly
     
    -    When pusing with SSH or other smart protocol, references are validated
    -    by function `check_to_send_update()` beforce they are sent in commands
    +    When pushing with SSH or other smart protocol, references are validated
    +    by function `check_to_send_update()` before they are sent in commands
         to `send_pack()` of "receve-pack".  For atomic push, if a reference is
         rejected after the validation, only references pushed by user should be
         marked as failure, instead of report failure on all remote references.
    @@ Commit message
         push_refs_with_push, 2019-07-11) wanted to fix report issue of HTTP
         protocol, but marked all remote references failure for atomic push.
     
    -    Revert part of that commit and add additional status for function
    -    `atomic_push_failure()`.  The additional status for it except the
    -    "REF_STATUS_EXPECTING_REPORT" status are:
    +    In order to fix the issue of status report for SSH or other built-in
    +    smart protocol, revert part of that commit and add additional status
    +    for function `atomic_push_failure()`.  The additional status for it
    +    except the "REF_STATUS_EXPECTING_REPORT" status are:
     
         - REF_STATUS_NONE : Not marked as "REF_STATUS_EXPECTING_REPORT" yet.
         - REF_STATUS_OK   : Assume OK for dryrun or status_report is disabled.
     
    -    This commit break test case in t5541, and will fix in other commit.
    +    This fix won't resolve the issue of status report in transport-helper
    +    for HTTP or other protocols, and breaks test case in t5541.  Will fix
    +    it in additional commit.
     
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
3:  50bb31ed3a ! 3:  82a8b19430 transport-helper: enforce atomic in push_refs_with_push
    @@ Metadata
     Author: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## Commit message ##
    -    transport-helper: enforce atomic in push_refs_with_push
    +    transport-helper: mark failure for atomic push
     
         Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
         push_refs_with_push, 2019-07-11) noticed the incomplete report of
-:  ---------- > 4:  fc8c1af33c transport-helper: new method reject_atomic_push()


Jiang Xin (4):
  t5543: never report what we do not push
  send-pack: mark failure of atomic push properly
  transport-helper: mark failure for atomic push
  transport-helper: new method reject_atomic_push()

 send-pack.c                | 27 ++----------
 t/t5541-http-push-smart.sh | 12 ++++-
 t/t5543-atomic-push.sh     | 89 ++++++++++++++++++++++++++++++++++++++
 transport-helper.c         | 23 ++++++++++
 transport.c                | 14 ------
 transport.h                |  3 ++
 6 files changed, 128 insertions(+), 40 deletions(-)

-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v2 1/4] t5543: never report what we do not push
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
@ 2020-03-29 14:33                 ` Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 2/4] send-pack: mark failure of atomic push properly Jiang Xin
                                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-29 14:33 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When we push some references to the git server, we expect git to report
the status of the references we are pushing; no more, no less.  But when
pusing with atomic mode, if some references cannot be pushed, Git reports
the reject message on all references in the remote repository.

Add new test cases in t5543, and fix them in latter commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5543-atomic-push.sh | 89 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 89 insertions(+)

diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 7079bcf9a0..001240eec7 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -27,6 +27,12 @@ test_refs () {
 	test_cmp expect actual
 }
 
+fmt_status_report () {
+	sed -n \
+		-e "/^To / { s/   */ /g; p; }" \
+		-e "/^ ! / { s/   */ /g; p; }"
+}
+
 test_expect_success 'atomic push works for a single branch' '
 	mk_repo_pair &&
 	(
@@ -191,4 +197,87 @@ test_expect_success 'atomic push is not advertised if configured' '
 	test_refs master HEAD@{1}
 '
 
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+# Atomic push            : master(2)               two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by update hook)' '
+	mk_repo_pair &&
+	(
+		cd workbench &&
+		test_commit one &&
+		git branch foo &&
+		git push up master one foo &&
+		git tag -d one
+	) &&
+	(
+		mkdir -p upstream/.git/hooks &&
+		cat >upstream/.git/hooks/update <<-EOF &&
+		#!/bin/sh
+
+		if test "\$1" = "refs/heads/bar"
+		then
+			echo >&2 "Pusing to branch bar is prohibited"
+			exit 1
+		fi
+		EOF
+		chmod a+x upstream/.git/hooks/update
+	) &&
+	(
+		cd workbench &&
+		test_commit two &&
+		git branch bar
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master two bar >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] two -> two (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+	(
+		cd workbench &&
+		git remote remove up &&
+		git remote add up ../upstream
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic --mirror up >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] one (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	 ! [remote rejected] two -> two (atomic push failure)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(2) one(1) foo(1)
+# References in workbench: master(1)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by non-ff)' '
+	rm upstream/.git/hooks/update &&
+	(
+		cd workbench &&
+		git push up master &&
+		git reset --hard HEAD^
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master foo bar >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [rejected] master -> master (non-fast-forward)
+	 ! [rejected] bar -> bar (atomic push failed)
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 2/4] send-pack: mark failure of atomic push properly
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 1/4] t5543: never report what we do not push Jiang Xin
@ 2020-03-29 14:33                 ` Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 3/4] transport-helper: mark failure for atomic push Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 4/4] transport-helper: new method reject_atomic_push() Jiang Xin
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-29 14:33 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing with SSH or other smart protocol, references are validated
by function `check_to_send_update()` before they are sent in commands
to `send_pack()` of "receve-pack".  For atomic push, if a reference is
rejected after the validation, only references pushed by user should be
marked as failure, instead of report failure on all remote references.

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) wanted to fix report issue of HTTP
protocol, but marked all remote references failure for atomic push.

In order to fix the issue of status report for SSH or other built-in
smart protocol, revert part of that commit and add additional status
for function `atomic_push_failure()`.  The additional status for it
except the "REF_STATUS_EXPECTING_REPORT" status are:

- REF_STATUS_NONE : Not marked as "REF_STATUS_EXPECTING_REPORT" yet.
- REF_STATUS_OK   : Assume OK for dryrun or status_report is disabled.

This fix won't resolve the issue of status report in transport-helper
for HTTP or other protocols, and breaks test case in t5541.  Will fix
it in additional commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c                |  2 ++
 t/t5541-http-push-smart.sh |  2 +-
 t/t5543-atomic-push.sh     |  6 +++---
 transport.c                | 14 --------------
 4 files changed, 6 insertions(+), 18 deletions(-)

diff --git a/send-pack.c b/send-pack.c
index 0407841ae8..ff016c468c 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -334,6 +334,8 @@ static int atomic_push_failure(struct send_pack_args *args,
 			continue;
 
 		switch (ref->status) {
+		case REF_STATUS_NONE:
+		case REF_STATUS_OK:
 		case REF_STATUS_EXPECTING_REPORT:
 			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
 			continue;
diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 23be8ce92d..2c2c3fb0f5 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,7 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
+test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 001240eec7..620c30d58f 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -200,7 +200,7 @@ test_expect_success 'atomic push is not advertised if configured' '
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
 # Atomic push            : master(2)               two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by update hook)' '
+test_expect_success 'atomic push reports (reject by update hook)' '
 	mk_repo_pair &&
 	(
 		cd workbench &&
@@ -241,7 +241,7 @@ test_expect_failure 'atomic push reports (reject by update hook)' '
 
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+test_expect_success 'atomic push reports (mirror, but reject by update hook)' '
 	(
 		cd workbench &&
 		git remote remove up &&
@@ -262,7 +262,7 @@ test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
 
 # References in upstream : master(2) one(1) foo(1)
 # References in workbench: master(1)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by non-ff)' '
+test_expect_success 'atomic push reports (reject by non-ff)' '
 	rm upstream/.git/hooks/update &&
 	(
 		cd workbench &&
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..75c5c9fe98 100644
--- a/transport.c
+++ b/transport.c
@@ -1240,20 +1240,6 @@ int transport_push(struct repository *r,
 		err = push_had_errors(remote_refs);
 		ret = push_ret | err;
 
-		if ((flags & TRANSPORT_PUSH_ATOMIC) && err) {
-			struct ref *it;
-			for (it = remote_refs; it; it = it->next)
-				switch (it->status) {
-				case REF_STATUS_NONE:
-				case REF_STATUS_UPTODATE:
-				case REF_STATUS_OK:
-					it->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-					break;
-				default:
-					break;
-				}
-		}
-
 		if (!quiet || err)
 			transport_print_push_status(transport->url, remote_refs,
 					verbose | porcelain, porcelain,
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 3/4] transport-helper: mark failure for atomic push
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
                                   ` (2 preceding siblings ...)
  2020-03-29 14:33                 ` [PATCH v2 2/4] send-pack: mark failure of atomic push properly Jiang Xin
@ 2020-03-29 14:33                 ` Jiang Xin
  2020-03-29 14:33                 ` [PATCH v2 4/4] transport-helper: new method reject_atomic_push() Jiang Xin
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-29 14:33 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) noticed the incomplete report of
failure of an atomic push for HTTP protocol.  But the implementation
has a flaw that mark all remote references as failure.

Only mark necessary references as failure in `push_refs_with_push()` of
transport-helper.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5541-http-push-smart.sh | 14 +++++++++++---
 transport-helper.c         | 15 +++++++++++++++
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 2c2c3fb0f5..afc680d5e3 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,10 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
+## References of remote: atomic1(1)            master(2) collateral(2) other(2)
+## References of local :            atomic2(2) master(1) collateral(3) other(2) collateral1(3) atomic(1)
+## Atomic push         :                       master(1) collateral(3)                         atomic(1)
+test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
@@ -189,7 +192,8 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 	test_commit atomic2 &&
 	git branch collateral &&
 	git branch other &&
-	git push "$up" master collateral other &&
+	git push "$up" atomic1 master collateral other &&
+	git tag -d atomic1 &&
 
 	# collateral is a valid push, but should be failed by atomic push
 	git checkout collateral &&
@@ -224,7 +228,11 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 
 	# the collateral failure refs should be indicated to the user
 	grep "^ ! .*rejected.* atomic -> atomic .*atomic push failed" output &&
-	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output
+	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output &&
+
+	# never report what we do not push
+	! grep "^ ! .*rejected.* atomic1 " output &&
+	! grep "^ ! .*rejected.* other " output
 '
 
 test_expect_success 'push --atomic fails on server-side errors' '
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..ab3b52eb14 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -894,6 +894,21 @@ static int push_refs_with_push(struct transport *transport,
 		case REF_STATUS_REJECT_STALE:
 		case REF_STATUS_REJECT_ALREADY_EXISTS:
 			if (atomic) {
+				/* Mark other refs as failed */
+				for (ref = remote_refs; ref; ref = ref->next) {
+					if (!ref->peer_ref && !mirror)
+						continue;
+
+					switch (ref->status) {
+					case REF_STATUS_NONE:
+					case REF_STATUS_OK:
+					case REF_STATUS_EXPECTING_REPORT:
+						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+						continue;
+					default:
+						break; /* do nothing */
+					}
+				}
 				string_list_clear(&cas_options, 0);
 				return 0;
 			} else
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 4/4] transport-helper: new method reject_atomic_push()
  2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
                                   ` (3 preceding siblings ...)
  2020-03-29 14:33                 ` [PATCH v2 3/4] transport-helper: mark failure for atomic push Jiang Xin
@ 2020-03-29 14:33                 ` Jiang Xin
  4 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-29 14:33 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new method in transport-helper to reject all references if any
reference is failed for atomic push.

This method is reused in "send-pack.c" and "transport-helper.c", one for
SSH, git and file protocols, and the other for HTTP protocol.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c        | 29 +++--------------------------
 transport-helper.c | 38 +++++++++++++++++++++++---------------
 transport.h        |  3 +++
 3 files changed, 29 insertions(+), 41 deletions(-)

diff --git a/send-pack.c b/send-pack.c
index ff016c468c..a2b8eaad0d 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -322,31 +322,6 @@ static int generate_push_cert(struct strbuf *req_buf,
 	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_NONE:
-		case REF_STATUS_OK:
-		case REF_STATUS_EXPECTING_REPORT:
-			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-			continue;
-		default:
-			break; /* do nothing */
-		}
-	}
-	return error("atomic push failed for ref %s. status: %d\n",
-		     failing_ref->name, failing_ref->status);
-}
-
 #define NONCE_LEN_LIMIT 256
 
 static void reject_invalid_nonce(const char *nonce, int len)
@@ -491,7 +466,9 @@ int send_pack(struct send_pack_args *args,
 			if (use_atomic) {
 				strbuf_release(&req_buf);
 				strbuf_release(&cap_buf);
-				return atomic_push_failure(args, remote_refs, ref);
+				reject_atomic_push(remote_refs, args->send_mirror);
+				return error("atomic push failed for ref %s. status: %d\n",
+					     ref->name, ref->status);
 			}
 			/* else fallthrough */
 		default:
diff --git a/transport-helper.c b/transport-helper.c
index ab3b52eb14..a46afcb69d 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -894,21 +894,7 @@ static int push_refs_with_push(struct transport *transport,
 		case REF_STATUS_REJECT_STALE:
 		case REF_STATUS_REJECT_ALREADY_EXISTS:
 			if (atomic) {
-				/* Mark other refs as failed */
-				for (ref = remote_refs; ref; ref = ref->next) {
-					if (!ref->peer_ref && !mirror)
-						continue;
-
-					switch (ref->status) {
-					case REF_STATUS_NONE:
-					case REF_STATUS_OK:
-					case REF_STATUS_EXPECTING_REPORT:
-						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-						continue;
-					default:
-						break; /* do nothing */
-					}
-				}
+				reject_atomic_push(remote_refs, mirror);
 				string_list_clear(&cas_options, 0);
 				return 0;
 			} else
@@ -1503,3 +1489,25 @@ int bidirectional_transfer_loop(int input, int output)
 
 	return tloop_spawnwait_tasks(&state);
 }
+
+void reject_atomic_push(struct ref *remote_refs, int mirror_mode)
+{
+	struct ref *ref;
+
+	/* Mark other refs as failed */
+	for (ref = remote_refs; ref; ref = ref->next) {
+		if (!ref->peer_ref && !mirror_mode)
+			continue;
+
+		switch (ref->status) {
+		case REF_STATUS_NONE:
+		case REF_STATUS_OK:
+		case REF_STATUS_EXPECTING_REPORT:
+			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+			continue;
+		default:
+			break; /* do nothing */
+		}
+	}
+	return;
+}
diff --git a/transport.h b/transport.h
index e0131daab9..4298c855be 100644
--- a/transport.h
+++ b/transport.h
@@ -265,4 +265,7 @@ int transport_refs_pushed(struct ref *ref);
 void transport_print_push_status(const char *dest, struct ref *refs,
 		  int verbose, int porcelain, unsigned int *reject_reasons);
 
+/* common method used by transport-helper.c and send-pack.c */
+void reject_atomic_push(struct ref *refs, int mirror_mode);
+
 #endif
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v2 0/4] Never report references we not push
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
@ 2020-03-29 14:35                   ` Jiang Xin
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
                                     ` (5 subsequent siblings)
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-29 14:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

Jiang Xin <worldhello.net@gmail.com> 于2020年3月29日周日 下午10:33写道:
>
> ## Changes since v1:
>
> * Add a new common method `reject_atomic_push()`, which is reused for
>   atomic push rejection for SSH and HTTP protocol.

I first try to add some common methods in `transport_push()` of
"transport.c", but I find it is hard to do this.  Because the
capabilities has not been set after calling
`transport->vtable->get_refs_list` for HTTP protocol.

And for the same reason, the following code in `send-pack.c` cannot
move to `set_ref_status_for_push()`.

    /*
     * NEEDSWORK: why does delete-refs have to be so specific to
     * send-pack machinery that set_ref_status_for_push() cannot
     * set this bit for us???
     */
    for (ref = remote_refs; ref; ref = ref->next)
        if (ref->deletion && !allow_deleting_refs)
                ref->status = REF_STATUS_REJECT_NODELETE

I wonder if we can extend method `get_refs_list()` of
"transport-helper.c"  and "remote-curl.c", so we can all method
`server_supports()` in `transport_push()` of "transport.c".

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v5 0/6] New proc-receive hook for centralized workflow
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (7 preceding siblings ...)
  2020-03-04 20:39 ` [PATCH 0/7] New execute-commands hook for centralized workflow Junio C Hamano
@ 2020-03-30 16:57 ` Jiang Xin
  2020-03-30 16:57 ` [PATCH v5 1/6] transport: not report a non-head push as a branch Jiang Xin
                   ` (13 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-30 16:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v4

* Fixed code review issues.
* Reimplement `read_proc_receive_result()` of patch 2/6.
* Add new parameter for function `print_ref_status()` of patch 5/6.
* Documentation in patch 6/6.


## Range-diff v4...v5

1:  7c08735833 ! 1:  a8bcc20439 transport: not report a non-head push as a branch
    @@ t/t5411-proc-receive-hook.sh (new)
     +#
     +#     create_commits_in <repo> A B C
     +#
    -+# NOTE: Avoid calling this function from a subshell since variable
    ++# NOTE: Never calling this function from a subshell since variable
     +# assignments will disappear when subshell exits.
     +create_commits_in () {
     +	repo="$1" &&
    -+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} 2>/dev/null)
    ++	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
     +	then
     +		parent=
     +	fi &&
    @@ t/t5411-proc-receive-hook.sh (new)
     +	git -C "$repo" update-ref refs/heads/master $oid
     +}
     +
    -+format_git_output () {
    ++# Format the output of git-push, git-show-ref and other commands to make a
    ++# user-friendly and stable text.  We can easily prepare the expect text
    ++# without having to worry about future changes of the commit ID and spaces
    ++# of the output.  We also replce single quotes with double quotes, because
    ++# it is boring to prepare unquoted single quotes in expect txt.
    ++make_user_friendly_and_stable_output () {
     +	sed \
    -+		-e "s/  *\$//g" \
    ++		-e "s/  *\$//" \
    ++		-e "s/   */ /g" \
    ++		-e "s/'/\"/g" \
     +		-e "s/$A/<COMMIT-A>/g" \
     +		-e "s/$B/<COMMIT-B>/g" \
    -+		-e "s/$TAG/<COMMIT-T>/g" \
    ++		-e "s/$TAG/<TAG-v123>/g" \
     +		-e "s/$ZERO_OID/<ZERO-OID>/g" \
    -+		-e "s/'/\"/g"
    ++		-e "s/[0-9a-f]\{7,\}/<OID>/g"
     +}
     +
    ++# Refs of upstream : master(B)  next(A)
    ++# Refs of workbench: master(A)           tags/v123
     +test_expect_success "setup" '
     +	git init --bare upstream &&
     +	git init workbench &&
     +	create_commits_in workbench A B &&
     +	(
     +		cd workbench &&
    -+		git remote add origin ../upstream &&
    ++		# Try to make a stable fixed width for abbreviated commit ID,
    ++		# this fixed-width oid will be replaced with "<OID>".
     +		git config core.abbrev 7 &&
    ++		git remote add origin ../upstream &&
     +		git update-ref refs/heads/master $A &&
    -+		git tag -m "v1.0.0" v1.0.0 $A &&
    ++		git tag -m "v123" v123 $A &&
     +		git push origin \
     +			$B:refs/heads/master \
     +			$A:refs/heads/next
     +	) &&
    -+	TAG=$(cd workbench; git rev-parse v1.0.0) &&
    ++	TAG=$(git -C workbench rev-parse v123) &&
     +
     +	# setup pre-receive hook
    -+	cat >upstream/hooks/pre-receive <<-EOF &&
    ++	cat >upstream/hooks/pre-receive <<-\EOF &&
     +	#!/bin/sh
     +
    -+	printf >&2 "# pre-receive hook\n"
    ++	echo >&2 "# pre-receive hook"
     +
     +	while read old new ref
     +	do
    -+		printf >&2 "pre-receive< \$old \$new \$ref\n"
    ++		echo >&2 "pre-receive< $old $new $ref"
     +	done
     +	EOF
     +
     +	# setup post-receive hook
    -+	cat >upstream/hooks/post-receive <<-EOF &&
    ++	cat >upstream/hooks/post-receive <<-\EOF &&
     +	#!/bin/sh
     +
    -+	printf >&2 "# post-receive hook\n"
    ++	echo >&2 "# post-receive hook"
     +
     +	while read old new ref
     +	do
    -+		printf >&2 "post-receive< \$old \$new \$ref\n"
    ++		echo >&2 "post-receive< $old $new $ref"
     +	done
     +	EOF
     +
    @@ t/t5411-proc-receive-hook.sh (new)
     +		upstream/hooks/post-receive
     +'
     +
    ++# Refs of upstream : master(B)  next(A)
    ++# Refs of workbench: master(A)           tags/v123
    ++# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
     +test_expect_success "normal git-push command" '
    -+	(
    -+		cd workbench &&
    -+		git push -f origin \
    -+			refs/tags/v1.0.0 \
    -+			:refs/heads/next \
    -+			HEAD:refs/heads/master \
    -+			HEAD:refs/review/master/topic \
    -+			HEAD:refs/heads/a/b/c
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	git -C workbench push -f origin \
    ++		refs/tags/v123 \
    ++		:refs/heads/next \
    ++		HEAD:refs/heads/master \
    ++		HEAD:refs/review/master/topic \
    ++		HEAD:refs/heads/a/b/c \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-T> refs/tags/v1.0.0
    ++	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
    -+	remote: post-receive< <ZERO-OID> <COMMIT-T> refs/tags/v1.0.0
    ++	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
     +	To ../upstream
    -+	 + ce858e6...1029397 HEAD -> master (forced update)
    -+	 - [deleted]         next
    -+	 * [new tag]         v1.0.0 -> v1.0.0
    -+	 * [new reference]   HEAD -> refs/review/master/topic
    -+	 * [new branch]      HEAD -> a/b/c
    ++	 + <OID>...<OID> HEAD -> master (forced update)
    ++	 - [deleted] next
    ++	 * [new tag] v123 -> v123
    ++	 * [new reference] HEAD -> refs/review/master/topic
    ++	 * [new branch] HEAD -> a/b/c
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/a/b/c
     +	<COMMIT-A> refs/heads/master
     +	<COMMIT-A> refs/review/master/topic
    -+	<COMMIT-T> refs/tags/v1.0.0
    ++	<TAG-v123> refs/tags/v123
     +	EOF
     +	test_cmp expect actual
     +'
     +
     +test_done
     
    + ## t/t5516-fetch-push.sh ##
    +@@ t/t5516-fetch-push.sh: test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
    + test_expect_success 'push --porcelain' '
    + 	mk_empty testrepo &&
    + 	echo >.git/foo  "To testrepo" &&
    +-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
    ++	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
    + 	echo >>.git/foo "Done" &&
    + 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
    + 	(
    +
      ## transport.c ##
     @@ transport.c: static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
    + 				 porcelain, summary_width);
      	else if (is_null_oid(&ref->old_oid))
      		print_ref_status('*',
    - 			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
    +-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
     -			"[new branch]"),
    -+			(starts_with(ref->name, "refs/heads/") ? "[new branch]" :
    -+			"[new reference]")),
    - 			ref, ref->peer_ref, NULL, porcelain, summary_width);
    +-			ref, ref->peer_ref, NULL, porcelain, summary_width);
    ++				 (starts_with(ref->name, "refs/tags/")
    ++				  ? "[new tag]"
    ++				  : (starts_with(ref->name, "refs/heads/")
    ++				     ? "[new branch]"
    ++				     : "[new reference]")),
    ++				 ref, ref->peer_ref, NULL, porcelain, summary_width);
      	else {
      		struct strbuf quickref = STRBUF_INIT;
    + 		char type;
2:  67dea721b7 ! 2:  14641ec57e receive-pack: add new proc-receive hook
    @@ Commit message
         We can use this "proc-receive" command to create pull requests or send
         emails for code review.
     
    -    This "proc-receive" hook reads commands, push-options (optional), and
    -    send result using a protocol in pkt-line format.  In the following
    -    example, The letter "S" stands for "receive-pack" and letter "H" stands
    -    for the hook.
    +    Suggested by Junio, this "proc-receive" hook reads the commands,
    +    push-options (optional), and send result using a protocol in pkt-line
    +    format.  In the following example, The letter "S" stands for
    +    "receive-pack" and letter "H" stands for the hook.
     
    -        S: PKT-LINE(version=1\0push-options ...)
    +        # Version and capabilities negotiation.
    +        S: PKT-LINE(version=1\0push-options atomic...)
             S: flush-pkt
    -
    -        H: PKT-LINE(version=1\0push-options ...)
    +        H: PKT-LINE(version=1\0push-options...)
             H: flush-pkt
     
    +        # Send commands from server to the hook.
             S: PKT-LINE(old-oid new-oid ref)
             S: ... ...
             S: flush-pkt
    -
    -        # Optional, only if push-options is negotiated.
    +        # Only if push-options have been negotiated.
             S: PKT-LINE(push-option)
             S: ... ...
             S: flush-pkt
     
    +        # Receive result from the hook.
             # OK, run this command successfully.
             H: PKT-LINE(old-oid new-oid ref ok)
    -
             # NO, I reject it.
             H: PKT-LINE(old-oid new-oid ref ng reason)
    -
             # OK, but use an alternate reference. (in latter commit)
             H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
    -
             # It will fallthrough to receive-pack to execute. (in latter commit)
             H: PKT-LINE(old-oid new-oid ref ft)
    -
             H: ... ...
             H: flush-pkt
     
    @@ Commit message
         the result to replace the commands that have specific `run_proc_receive`
         field turned on.
     
    +    Suggested-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## Makefile ##
    @@ Makefile: TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
      TEST_BUILTINS_OBJS += test-read-cache.o
     
      ## builtin/receive-pack.c ##
    -@@ builtin/receive-pack.c: struct command {
    +@@ builtin/receive-pack.c: static void write_head_info(void)
    + 	packet_flush(1);
    + }
    + 
    ++#define RUN_PROC_RECEIVE_SCHEDULE	1
    ++#define RUN_PROC_RECEIVE_RETURNED	2
    + struct command {
      	struct command *next;
      	const char *error_string;
      	unsigned int skip_update:1,
     -		     did_not_exist:1;
     +		     did_not_exist:1,
    -+		     run_proc_receive:1;
    ++		     run_proc_receive:2;
      	int index;
      	struct object_id old_oid;
      	struct object_id new_oid;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
      	return finish_command(&proc);
      }
      
    ++static struct command *find_command_by_refname(const struct command *list,
    ++					       const char *refname)
    ++{
    ++	for ( ; list; list = list->next)
    ++		if (!strcmp(list->ref_name, refname))
    ++			return (struct command *)list;
    ++	return NULL;
    ++}
    ++
     +static int read_proc_receive_result(struct packet_reader *reader,
    -+				    struct command **commands)
    ++				    struct command *commands)
     +{
    -+	struct command **tail = commands;
    ++	struct command *hint;
    ++	struct command *cmd;
     +	int code = 0;
     +
    ++	hint = NULL;
     +	for (;;) {
     +		struct object_id old_oid, new_oid;
    -+		struct command *cmd;
     +		const char *refname;
     +		const char *p;
     +		char *status;
     +		char *msg = NULL;
     +
    -+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
    ++		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
     +			break;
    -+		}
    -+
     +		if (parse_oid_hex(reader->line, &old_oid, &p) ||
     +		    *p++ != ' ' ||
     +		    parse_oid_hex(p, &new_oid, &p) ||
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			die("protocol error: proc-receive has bad status '%s' for '%s'",
     +			    status, reader->line);
     +
    -+		FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname));
    -+		oidcpy(&cmd->old_oid, &old_oid);
    -+		oidcpy(&cmd->new_oid, &new_oid);
    -+		cmd->run_proc_receive = 1;
    -+
    ++		/* first try searching at our hint, falling back to all refs */
    ++		if (hint)
    ++			hint = find_command_by_refname(hint, refname);
    ++		if (!hint)
    ++			hint = find_command_by_refname(commands, refname);
    ++		if (!hint) {
    ++			warning("proc-receive reported status on unknown ref: %s",
    ++				refname);
    ++			continue;
    ++		}
    ++		if (!hint->run_proc_receive) {
    ++			warning("proc-receive reported status on ref of builtin command: %s",
    ++				refname);
    ++			continue;
    ++		}
    ++		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
    ++		oidcpy(&hint->old_oid, &old_oid);
    ++		oidcpy(&hint->new_oid, &new_oid);
     +		if (!strcmp(status, "ng")) {
     +			if (msg)
    -+				cmd->error_string = xstrdup(msg);
    ++				hint->error_string = xstrdup(msg);
     +			else
    -+				cmd->error_string = "failed";
    ++				hint->error_string = "failed";
     +			code = 1;
     +		} else if (strcmp("ok", status)) {
     +			die("protocol error: proc-receive has bad status '%s' for '%s'",
     +			    status, reader->line);
     +		}
    -+
    -+		*tail = cmd;
    -+		tail = &cmd->next;
     +	}
    ++
    ++	for (cmd = commands; cmd; cmd = cmd->next)
    ++		if (cmd->run_proc_receive &&
    ++		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED))
    ++		    cmd->error_string = "no report from proc-receive";
    ++
     +	return code;
     +}
     +
    -+static int run_proc_receive_hook(struct command **commands,
    ++static int run_proc_receive_hook(struct command *commands,
     +				 const struct string_list *push_options)
     +{
     +	struct child_process proc = CHILD_PROCESS_INIT;
     +	struct async muxer;
    -+	struct command *result_commands = NULL;
     +	struct command *cmd;
     +	const char *argv[2];
     +	struct packet_reader reader;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	packet_reader_init(&reader, proc.out, NULL, 0,
     +			   PACKET_READ_CHOMP_NEWLINE |
     +			   PACKET_READ_DIE_ON_ERR_PACKET);
    -+	if (use_push_options)
    -+		strbuf_addstr(&cap, " push-options");
     +	if (use_atomic)
     +		strbuf_addstr(&cap, " atomic");
    ++	if (use_push_options)
    ++		strbuf_addstr(&cap, " push-options");
     +	if (cap.len) {
     +		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
     +		strbuf_release(&cap);
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		die("protocol error: unknown proc-receive version '%d'", version);
     +
     +	/* Send commands */
    -+	for (cmd = *commands; cmd; cmd = cmd->next) {
    ++	for (cmd = commands; cmd; cmd = cmd->next) {
     +		char *old_hex, *new_hex;
     +
     +		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	}
     +
     +	/* Read result from proc-receive */
    -+	code = read_proc_receive_result(&reader, &result_commands);
    ++	code = read_proc_receive_result(&reader, commands);
     +	close(proc.in);
     +	close(proc.out);
     +	if (use_sideband)
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +	sigchain_pop(SIGPIPE);
     +
    -+	/* After receiving the result from the "proc-receive" hook,
    -+	 * "receive-pack" will use the result to replace commands that
    -+	 * have specific `run_proc_receive` field.
    -+	 */
    -+	for (cmd = *commands; cmd; cmd = cmd->next)
    -+		if (!cmd->run_proc_receive)
    -+			break;
    -+
    -+	/* Merge commands with result_commands and sort */
    -+	if (!cmd) {
    -+		*commands = result_commands;
    -+	} else {
    -+		struct command *next_cmd = cmd;
    -+		struct command *next_result = result_commands;
    -+		struct command *head = NULL;
    -+		struct command *tail = NULL;
    -+
    -+		if (!next_result ||
    -+		    strcmp(next_cmd->ref_name, next_result->ref_name) < 0) {
    -+			head = next_cmd;
    -+			next_cmd = next_cmd->next;
    -+		} else {
    -+			head = next_result;
    -+			next_result = next_result->next;
    -+		}
    -+		tail = head;
    -+
    -+		for (;;) {
    -+			if (!next_cmd) {
    -+				tail->next = next_result;
    -+				break;
    -+			} else if (next_cmd->run_proc_receive) {
    -+				next_cmd = next_cmd->next;
    -+			} else if (!next_result) {
    -+				tail->next = next_cmd;
    -+				next_cmd = next_cmd->next;
    -+				tail = tail->next;
    -+			} else {
    -+				if (strcmp(next_cmd->ref_name, next_result->ref_name) < 0) {
    -+					tail->next = next_cmd;
    -+					next_cmd = next_cmd->next;
    -+					tail = tail->next;
    -+				} else {
    -+					tail->next = next_result;
    -+					next_result = next_result->next;
    -+					tail = tail->next;
    -+				}
    -+			}
    -+		}
    -+		*commands = head;
    -+	}
    -+
     +	return code;
     +}
     +
    @@ builtin/receive-pack.c: static void execute_commands_atomic(struct command *comm
      			continue;
      
      		cmd->error_string = update(cmd, si);
    -@@ builtin/receive-pack.c: static void execute_commands_atomic(struct command *commands,
    - 	strbuf_release(&err);
    - }
    - 
    --static void execute_commands(struct command *commands,
    -+static void execute_commands(struct command **orig_commands,
    - 			     const char *unpacker_error,
    - 			     struct shallow_info *si,
    - 			     const struct string_list *push_options)
    - {
    -+	struct command *commands = *orig_commands;
    - 	struct check_connected_options opt = CHECK_CONNECTED_INIT;
    - 	struct command *cmd;
    +@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      	struct iterate_data data;
      	struct async muxer;
      	int err_fd = 0;
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +	if (run_proc_receive) {
     +		int code;
     +
    -+		code = run_proc_receive_hook(orig_commands, push_options);
    -+		commands = *orig_commands;
    ++		code = run_proc_receive_hook(commands, push_options);
     +		if (code) {
     +			for (cmd = commands; cmd; cmd = cmd->next) {
     +				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      	if (use_atomic)
      		execute_commands_atomic(commands, si);
      	else
    -@@ builtin/receive-pack.c: int cmd_receive_pack(int argc, const char **argv, const char *prefix)
    - 			update_shallow_info(commands, &si, &ref);
    - 		}
    - 		use_keepalive = KEEPALIVE_ALWAYS;
    --		execute_commands(commands, unpack_status, &si,
    -+		execute_commands(&commands, unpack_status, &si,
    - 				 &push_options);
    - 		if (pack_lockfile)
    - 			unlink_or_warn(pack_lockfile);
     
      ## t/helper/test-proc-receive.c (new) ##
     @@
    @@ t/helper/test-proc-receive.c (new)
     +static int version = 1;
     +static int verbose = 0;
     +static int no_push_options = 0;
    ++static int use_atomic = 0;
     +static int use_push_options = 0;
     +static struct string_list returns = STRING_LIST_INIT_NODUP;
     +
    @@ t/helper/test-proc-receive.c (new)
     +			linelen = strlen(reader->line);
     +			if (linelen < reader->pktlen) {
     +				const char *feature_list = reader->line + linelen + 1;
    ++				if (parse_feature_request(feature_list, "atomic"))
    ++					use_atomic= 1;
     +				if (parse_feature_request(feature_list, "push-options"))
     +					use_push_options = 1;
     +			}
    @@ t/helper/test-proc-receive.c (new)
     +	if (verbose) {
     +		struct command *cmd;
     +
    ++		if (use_push_options || use_atomic)
    ++			fprintf(stderr, "proc-receive:%s%s\n",
    ++				use_atomic? " atomic": "",
    ++				use_push_options ? " push_options": "");
    ++
     +		for (cmd = commands; cmd; cmd = cmd->next) {
     +			char *old_hex, *new_hex;
     +
    @@ t/helper/test-tool.h: int cmd__parse_pathspec_file(int argc, const char** argv);
      int cmd__read_cache(int argc, const char **argv);
     
      ## t/t5411-proc-receive-hook.sh ##
    -@@ t/t5411-proc-receive-hook.sh: format_git_output () {
    - 		-e "s/'/\"/g"
    +@@ t/t5411-proc-receive-hook.sh: make_user_friendly_and_stable_output () {
    + 		-e "s/[0-9a-f]\{7,\}/<OID>/g"
      }
      
     +# Asynchronous sideband may generate inconsistent output messages,
     +# sort before comparison.
     +test_sorted_cmp () {
    -+	if ! $GIT_TEST_CMP "$@"
    ++	if ! $GIT_TEST_CMP "$@" >/dev/null 2>&1
     +	then
     +		cmd=$GIT_TEST_CMP
     +		for f in "$@"
    @@ t/t5411-proc-receive-hook.sh: format_git_output () {
     +			sort "$f" >"$f.sorted"
     +			cmd="$cmd \"$f.sorted\""
     +		done
    -+		if ! eval $cmd
    ++		if ! eval $cmd >/dev/null 2>&1
     +		then
     +			$GIT_TEST_CMP "$@"
     +		fi
     +	fi
     +}
     +
    + # Refs of upstream : master(B)  next(A)
    + # Refs of workbench: master(A)           tags/v123
      test_expect_success "setup" '
    - 	git init --bare upstream &&
    - 	git init workbench &&
     @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
      	test_cmp expect actual
      '
      
    ++# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
    ++# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
     +	(
     +		cd upstream &&
     +		git update-ref -d refs/review/master/topic &&
    -+		git update-ref -d refs/tags/v1.0.0 &&
    ++		git update-ref -d refs/tags/v123 &&
     +		git update-ref -d refs/heads/a/b/c
     +	)
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       next(A)  refs/for/master/topic(A)
     +test_expect_success "no proc-receive hook, fail to push special ref" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:next \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:next \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	To ../upstream
    -+	 * [new branch]      HEAD -> next
    ++	 * [new branch] HEAD -> next
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	<COMMIT-A> refs/heads/next
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
    -+	(
    -+		cd upstream &&
    -+		git update-ref -d refs/heads/next
    -+	)
    ++	git -C upstream update-ref -d refs/heads/next
     +'
     +
    -+# TODO: report for the failure of master branch is unnecessary.
    -+test_expect_success "no proc-receive hook, fail all for atomic push" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push --atomic origin \
    -+			HEAD:next \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push --atomic:                       next(A)  refs/for/master/topic(A)
    ++test_expect_failure "no proc-receive hook, fail all for atomic push" '
    ++	test_must_fail git -C workbench push --atomic origin \
    ++		HEAD:next \
    ++		HEAD:refs/for/master/topic >out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: error: cannot to find hook "proc-receive"
     +	To ../upstream
    -+	 ! [rejected]        master (atomic push failed)
     +	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	chmod a+x upstream/hooks/proc-receive
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(A)
     +test_expect_success "proc-receive bad protocol: unknown version" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out | grep "protocol error" >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
     +	cat >expect <<-EOF &&
     +	fatal: protocol error: unknown proc-receive version "2"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       next(A)  refs/for/master/topic(A)
     +test_expect_success "proc-receive bad protocol: no report" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/heads/next \
    ++		HEAD:refs/for/master/topic >out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	To ../upstream
    -+	 ! [remote failure]  HEAD -> refs/for/master/topic (remote failed to report status)
    ++	 * [new branch] HEAD -> next
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
     +	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    ++	<COMMIT-A> refs/heads/next
     +	EOF
     +	test_cmp expect actual
     +'
     +
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup" '
    ++	git -C upstream update-ref -d refs/heads/next
    ++
    ++'
    ++
     +test_expect_success "setup proc-receive hook (bad oid)" '
     +	cat >upstream/hooks/proc-receive <<-EOF
     +	#!/bin/sh
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
     +test_expect_success "proc-receive bad protocol: bad oid" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out | grep "protocol error" >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic\
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
     +	cat >expect <<-EOF &&
     +	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
     +test_expect_success "proc-receive bad protocol: no status" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out | grep "protocol error" >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
     +	cat >expect <<-EOF &&
     +	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
     +test_expect_success "proc-receive bad protocol: unknown status" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out | grep "protocol error" >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++			HEAD:refs/for/master/topic \
    ++			>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
     +	cat >expect <<-EOF &&
     +	fatal: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
     +test_expect_success "proc-receive bad protocol: bad status" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out | grep "protocol error" >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
     +	cat >expect <<-EOF &&
     +	fatal: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
     +test_expect_success "proc-receive: fail to update (no message)" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
     +test_expect_success "proc-receive: fail to update (has message)" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
     +'
     +
    ++test_expect_success "setup proc-receive hook (report status on builtin command)" '
    ++	cat >upstream/hooks/proc-receive <<-EOF
    ++	#!/bin/sh
    ++
    ++	printf >&2 "# proc-receive hook\n"
    ++
    ++	test-tool proc-receive -v \
    ++		-r "$ZERO_OID $A refs/heads/master ok"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         : (B)                   refs/for/master/topic
    ++test_expect_success "proc-receive: warning on report for builtin command" '
    ++	test_must_fail git -C workbench push origin \
    ++		$B:refs/heads/master \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
    ++	warning: proc-receive reported status on ref of builtin command: refs/heads/master
    ++	remote: # post-receive hook
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	To ../upstream
    ++	 <OID>..<OID> <COMMIT-B> -> master
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    ++	error: failed to push some refs to "../upstream"
    ++	EOF
    ++	test_sorted_cmp expect actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-B> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "cleanup" '
    ++	git -C upstream update-ref refs/heads/master $A
    ++'
    ++
     +test_expect_success "setup proc-receive hook (ok)" '
     +	cat >upstream/hooks/proc-receive <<-EOF
     +	#!/bin/sh
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	EOF
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
     +test_expect_success "proc-receive: ok" '
    -+	(
    -+		cd workbench &&
    -+		git push origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To ../upstream
    -+	 * [new reference]   HEAD -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "proc-receive: report unknown ref" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push origin \
    -+			HEAD:refs/for/a/b/c/my/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/a/b/c/my/topic
    ++test_expect_success "proc-receive: no report from proc-receive" '
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/a/b/c/my/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    -+	warning: remote reported status on unknown ref: refs/for/master/topic
    -+	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	warning: proc-receive reported status on unknown ref: refs/for/master/topic
     +	To ../upstream
    -+	 ! [remote failure]  HEAD -> refs/for/a/b/c/my/topic (remote failed to report status)
    ++	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
     +	error: failed to push some refs to "../upstream"
     +	EOF
    -+	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push -o ...  :                       refs/for/master/topic
     +test_expect_success "not support push options" '
    -+	(
    -+		cd workbench &&
    -+		test_must_fail git push \
    -+			-o issue=123 \
    -+			-o reviewer=user1 \
    -+			origin \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	test_must_fail git -C workbench push \
    ++		-o issue=123 \
    ++		-o reviewer=user1 \
    ++		origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	fatal: the receiving end does not support push options
     +	fatal: the remote end hung up unexpectedly
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "enable push options" '
    -+	(
    -+		cd upstream &&
    -+		git config receive.advertisePushOptions true
    -+	)
    ++	git -C upstream config receive.advertisePushOptions true
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push -o ...  :                       next(A)  refs/for/master/topic
     +test_expect_success "push with options" '
    -+	(
    -+		cd workbench &&
    -+		git push \
    -+			-o issue=123 \
    -+			-o reviewer=user1 \
    -+			origin \
    -+			HEAD:refs/heads/next \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	git -C workbench push \
    ++		--atomic \
    ++		-o issue=123 \
    ++		-o reviewer=user1 \
    ++		origin \
    ++		HEAD:refs/heads/next \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
    ++	remote: proc-receive: atomic push_options
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive< issue=123
     +	remote: proc-receive< reviewer=user1
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
     +	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To ../upstream
    -+	 * [new branch]      HEAD -> next
    -+	 * [new reference]   HEAD -> refs/for/master/topic
    ++	 * [new branch] HEAD -> next
    ++	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	<COMMIT-A> refs/heads/next
3:  d3d4ee428b = 3:  2440a474bd refs.c: refactor to reuse ref_is_hidden()
4:  5e049d89b1 ! 4:  e041582643 receive-pack: new config receive.procReceiveRefs
    @@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const ch
      	return git_default_config(var, value, cb);
      }
      
    -@@ builtin/receive-pack.c: static void execute_commands(struct command **orig_commands,
    +@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      	/* Try to find commands that have special prefix in their reference names,
      	 * and mark them to run an external "proc-receive" hook later.
      	 */
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "cleanup" '
     +	)
     +'
     +
    - test_expect_success "no proc-receive hook, fail to push special ref" '
    - 	(
    - 		cd workbench &&
    + # Refs of upstream : master(A)
    + # Refs of workbench: master(A)  tags/v123
    + # git push         :                       next(A)  refs/for/master/topic(A)
     @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
      	test_cmp expect actual
      '
      
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
    -+	(
    -+		cd upstream &&
    -+		git update-ref -d refs/heads/next
    -+	)
    ++	git -C upstream update-ref -d refs/heads/next
     +'
     +
     +test_expect_success "setup proc-receive hook" '
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +	chmod a+x upstream/hooks/proc-receive
     +'
     +
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
     +test_expect_success "report update of all special refs" '
    -+	(
    -+		cd workbench &&
    -+		git push origin \
    -+			HEAD:refs/for/next/topic \
    -+			HEAD:refs/review/a/b/c/topic \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/next/topic \
    ++		HEAD:refs/review/a/b/c/topic \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To ../upstream
    -+	 * [new reference]   HEAD -> refs/for/next/topic
    -+	 * [new reference]   HEAD -> refs/review/a/b/c/topic
    -+	 * [new reference]   HEAD -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/for/next/topic
    ++	 * [new reference] HEAD -> refs/review/a/b/c/topic
    ++	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
     +	EOF
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +	chmod a+x upstream/hooks/proc-receive
     +'
     +
    -+test_expect_success "report mixed refs update (head first)" '
    -+	(
    -+		cd workbench &&
    -+		git push origin \
    -+			HEAD:refs/heads/zzz \
    -+			HEAD:refs/for/next/topic \
    -+			HEAD:refs/heads/yyy \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
    ++test_expect_success "report mixed refs update" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/heads/bar \
    ++		HEAD:refs/heads/baz \
    ++		HEAD:refs/for/next/topic \
    ++		HEAD:refs/heads/foo \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/zzz
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
     +	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/zzz
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
     +	To ../upstream
    -+	 * [new branch]      HEAD -> zzz
    -+	 * [new reference]   HEAD -> refs/for/next/topic
    -+	 * [new branch]      HEAD -> yyy
    -+	 * [new reference]   HEAD -> refs/for/master/topic
    ++	 * [new branch] HEAD -> bar
    ++	 * [new branch] HEAD -> baz
    ++	 * [new reference] HEAD -> refs/for/next/topic
    ++	 * [new branch] HEAD -> foo
    ++	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	<COMMIT-A> refs/heads/master
    -+	<COMMIT-A> refs/heads/yyy
    -+	<COMMIT-A> refs/heads/zzz
    -+	EOF
    -+	test_cmp expect actual
    -+'
    -+
    -+test_expect_success "cleanup" '
    -+	(
    -+		cd upstream &&
    -+		git update-ref -d refs/heads/yyy &&
    -+		git update-ref -d refs/heads/zzz
    -+	)
    -+'
    -+
    -+test_expect_success "setup proc-receive hook" '
    -+	cat >upstream/hooks/proc-receive <<-EOF &&
    -+	#!/bin/sh
    -+
    -+	printf >&2 "# proc-receive hook\n"
    -+
    -+	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/next/topic ok" \
    -+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
    -+		-r "$ZERO_OID $A refs/for/master/topic ok"
    -+	EOF
    -+	chmod a+x upstream/hooks/proc-receive
    -+'
    -+
    -+test_expect_success "report mixed refs update (special ref first)" '
    -+	(
    -+		cd workbench &&
    -+		git push origin \
    -+			HEAD:refs/for/next/topic \
    -+			$B:refs/heads/zzz \
    -+			HEAD:refs/review/a/b/c/topic \
    -+			HEAD:refs/heads/yyy \
    -+			HEAD:refs/for/master/topic
    -+	) >out 2>&1 &&
    -+	format_git_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/heads/zzz
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    -+	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/heads/zzz
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/yyy
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	To ../upstream
    -+	 * [new reference]   HEAD -> refs/for/next/topic
    -+	 * [new branch]      <COMMIT-B> -> zzz
    -+	 * [new reference]   HEAD -> refs/review/a/b/c/topic
    -+	 * [new branch]      HEAD -> yyy
    -+	 * [new reference]   HEAD -> refs/for/master/topic
    -+	EOF
    -+	test_cmp expect actual &&
    -+	(
    -+		cd upstream &&
    -+		git show-ref
    -+	) >out &&
    -+	format_git_output <out >actual &&
    ++	git -C upstream show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/bar
    ++	<COMMIT-A> refs/heads/baz
    ++	<COMMIT-A> refs/heads/foo
     +	<COMMIT-A> refs/heads/master
    -+	<COMMIT-A> refs/heads/yyy
    -+	<COMMIT-B> refs/heads/zzz
     +	EOF
     +	test_cmp expect actual
     +'
5:  624538c1b7 < -:  ---------- receive-pack: refactor report for proc-receive
-:  ---------- > 5:  439acbdb62 receive-pack: refactor report for proc-receive
6:  56543573cf = 6:  24e5cddee0 doc: add documentation for the proc-receive hook


Jiang Xin (6):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  receive-pack: refactor report for proc-receive
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt |  14 +
 Documentation/githooks.txt       |  70 +++
 Makefile                         |   1 +
 builtin/receive-pack.c           | 290 +++++++++-
 refs.c                           |  11 +-
 refs.h                           |   1 +
 t/helper/test-proc-receive.c     | 172 ++++++
 t/helper/test-tool.c             |   1 +
 t/helper/test-tool.h             |   1 +
 t/t5411-proc-receive-hook.sh     | 916 +++++++++++++++++++++++++++++++
 t/t5516-fetch-push.sh            |   2 +-
 transport-helper.c               |  64 +--
 transport.c                      |  62 ++-
 13 files changed, 1539 insertions(+), 66 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh

-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v5 1/6] transport: not report a non-head push as a branch
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (8 preceding siblings ...)
  2020-03-30 16:57 ` [PATCH v5 0/6] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-03-30 16:57 ` Jiang Xin
  2020-03-30 16:57 ` [PATCH v5 2/6] receive-pack: add new proc-receive hook Jiang Xin
                   ` (12 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-30 16:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh | 153 +++++++++++++++++++++++++++++++++++
 t/t5516-fetch-push.sh        |   2 +-
 transport.c                  |   9 ++-
 3 files changed, 160 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..1784bcb584
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,153 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To ../upstream
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..272c0f4046 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v5 2/6] receive-pack: add new proc-receive hook
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (9 preceding siblings ...)
  2020-03-30 16:57 ` [PATCH v5 1/6] transport: not report a non-head push as a branch Jiang Xin
@ 2020-03-30 16:57 ` Jiang Xin
  2020-03-31  0:19   ` Junio C Hamano
  2020-03-31  0:21   ` Junio C Hamano
  2020-03-30 16:57 ` [PATCH v5 3/6] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                   ` (11 subsequent siblings)
  22 siblings, 2 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-30 16:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and capabilities negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt
    # Only if push-options have been negotiated.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(old-oid new-oid ref ok)
    # NO, I reject it.
    H: PKT-LINE(old-oid new-oid ref ng reason)
    # OK, but use an alternate reference. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
    # It will fallthrough to receive-pack to execute. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ft)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook can create/update another alternate
reference.  For example, a command for a reference "refs/for/master" may
create a special reference, such as "refs/pull/123/head".  The alternate
reference can be returned from the result in an extensible format like
"<old-oid> <new-oid> <reference> <status> [<message>]".

The result will be stored in a command list, and "receive-pack" will use
the result to replace the commands that have specific `run_proc_receive`
field turned on.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 242 ++++++++++++++-
 t/helper/test-proc-receive.c | 172 +++++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411-proc-receive-hook.sh | 555 +++++++++++++++++++++++++++++++++++
 6 files changed, 969 insertions(+), 3 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index 9804a0758b..0a08a0504e 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..7dbb05df8c 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULE	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +820,212 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for ( ; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_result(struct packet_reader *reader,
+				    struct command *commands)
+{
+	struct command *hint;
+	struct command *cmd;
+	int code = 0;
+
+	hint = NULL;
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+			    reader->line);
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status)
+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+			    reader->line);
+		*status++ = '\0';
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2)
+			die("protocol error: proc-receive has bad status '%s' for '%s'",
+			    status, reader->line);
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			warning("proc-receive reported status on unknown ref: %s",
+				refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			warning("proc-receive reported status on ref of builtin command: %s",
+				refname);
+			continue;
+		}
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		oidcpy(&hint->old_oid, &old_oid);
+		oidcpy(&hint->new_oid, &new_oid);
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				hint->error_string = xstrdup(msg);
+			else
+				hint->error_string = "failed";
+			code = 1;
+		} else if (strcmp("ok", status)) {
+			die("protocol error: proc-receive has bad status '%s' for '%s'",
+			    status, reader->line);
+		}
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED))
+		    cmd->error_string = "no report from proc-receive";
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot to find hook 'proc-receive'");
+		return 1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1)
+		die("protocol error: unknown proc-receive version '%d'", version);
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		char *old_hex, *new_hex;
+
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+
+		old_hex = oid_to_hex(&cmd->old_oid);
+		new_hex = oid_to_hex(&cmd->new_oid);
+
+		packet_write_fmt(proc.in, "%s %s %s",
+				 old_hex, new_hex, cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_result(&reader, commands);
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		die("proc-receive did not exit properly");
+
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1601,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1641,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1677,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1707,20 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = 1;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1747,18 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive) {
+		int code;
+
+		code = run_proc_receive_hook(commands, push_options);
+		if (code) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+					cmd->error_string = "fail to run proc-receive hook";
+			}
+		}
+	}
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..aafa3e8e59
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,172 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname));
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			char *old_hex, *new_hex;
+
+			old_hex = oid_to_hex(&cmd->old_oid);
+			new_hex = oid_to_hex(&cmd->new_oid);
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				old_hex, new_hex, cmd->ref_name);
+		}
+
+		if (push_options.nr > 0) {
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+		}
+
+		if (returns.nr) {
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+		}
+	}
+
+	if (returns.nr) {
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	}
+	packet_flush(1);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index c9a232d238..9bff461446 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -43,6 +43,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index c8549fd87f..71e5f1c372 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -33,6 +33,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 1784bcb584..79a3e7fde7 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -57,6 +57,24 @@ make_user_friendly_and_stable_output () {
 		-e "s/[0-9a-f]\{7,\}/<OID>/g"
 }
 
+# Asynchronous sideband may generate inconsistent output messages,
+# sort before comparison.
+test_sorted_cmp () {
+	if ! $GIT_TEST_CMP "$@" >/dev/null 2>&1
+	then
+		cmd=$GIT_TEST_CMP
+		for f in "$@"
+		do
+			sort "$f" >"$f.sorted"
+			cmd="$cmd \"$f.sorted\""
+		done
+		if ! eval $cmd >/dev/null 2>&1
+		then
+			$GIT_TEST_CMP "$@"
+		fi
+	fi
+}
+
 # Refs of upstream : master(B)  next(A)
 # Refs of workbench: master(A)           tags/v123
 test_expect_success "setup" '
@@ -150,4 +168,541 @@ test_expect_success "normal git-push command" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd upstream &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot to find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To ../upstream
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C upstream update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic:                       next(A)  refs/for/master/topic(A)
+test_expect_failure "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot to find hook "proc-receive"
+	To ../upstream
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: unknown proc-receive version "2"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To ../upstream
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C upstream update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
+	cat >expect <<-EOF &&
+	fatal: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To ../upstream
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To ../upstream
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	warning: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To ../upstream
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C upstream update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >upstream/hooks/proc-receive <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	warning: proc-receive reported status on unknown ref: refs/for/master/topic
+	To ../upstream
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	error: failed to push some refs to "../upstream"
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C upstream config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v5 3/6] refs.c: refactor to reuse ref_is_hidden()
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (10 preceding siblings ...)
  2020-03-30 16:57 ` [PATCH v5 2/6] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-03-30 16:57 ` Jiang Xin
  2020-03-30 16:57 ` [PATCH v5 4/6] receive-pack: new config receive.procReceiveRefs Jiang Xin
                   ` (10 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-30 16:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v5 4/6] receive-pack: new config receive.procReceiveRefs
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (11 preceding siblings ...)
  2020-03-30 16:57 ` [PATCH v5 3/6] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-03-30 16:57 ` Jiang Xin
  2020-03-30 16:57 ` [PATCH v5 5/6] receive-pack: refactor report for proc-receive Jiang Xin
                   ` (9 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-30 16:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 ++++
 builtin/receive-pack.c           |  43 ++++++++--
 t/t5411-proc-receive-hook.sh     | 130 +++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..0178f2d478 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for/", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 7dbb05df8c..21b3a1d3fa 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1710,15 +1725,26 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = 1;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = 1;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2178,6 +2204,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2293,5 +2321,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 79a3e7fde7..4c3699fa85 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -179,6 +179,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd upstream &&
+		git config --add receive.procReceiveRefs refs/for/ &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -705,4 +713,126 @@ test_expect_success "push with options" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C upstream update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To ../upstream
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v5 5/6] receive-pack: refactor report for proc-receive
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (12 preceding siblings ...)
  2020-03-30 16:57 ` [PATCH v5 4/6] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-03-30 16:57 ` Jiang Xin
  2020-03-30 16:57 ` [PATCH v5 6/6] doc: add documentation for the proc-receive hook Jiang Xin
                   ` (8 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-30 16:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The "proc-receive" may update one or more references, and will send its
result one by one in pkt-line format.  Each line of the result has four
fields and one optional message field, as "<old-oid> <new-oid> <ref>
<status> [<message>]".  See the following example:

    # OK, run this command successfully.
    PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference.
    PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute.
    PKT-LINE(old-oid new-oid ref ft)

The first three fields have the same foramt as a command.

The forth field has a two-letter status code.  Available status code:

* ok: The command runs successfully.  If the optional message has a
  prefix "ref:", the hook has created/updated an alternate reference
  instead.

* ng: Fail to run the command. Error message is in the optional message
  field.

* ft: Will fallthrough to receive-pack to execute.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 21 +++++++--
 t/t5411-proc-receive-hook.sh | 90 +++++++++++++++++++++++++++++++++---
 transport-helper.c           | 64 ++++++++++++-------------
 transport.c                  | 59 +++++++++++++----------
 4 files changed, 168 insertions(+), 66 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 21b3a1d3fa..5e98eac3aa 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -328,6 +328,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extra_string;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     run_proc_receive:2;
@@ -906,7 +907,12 @@ static int read_proc_receive_result(struct packet_reader *reader,
 			else
 				hint->error_string = "failed";
 			code = 1;
-		} else if (strcmp("ok", status)) {
+		} else if (!strcmp("ok", status)) {
+			hint->extra_string = xstrdup_or_null(msg);
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else {
 			die("protocol error: proc-receive has bad status '%s' for '%s'",
 			    status, reader->line);
 		}
@@ -2161,12 +2167,17 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
+		if (!cmd->error_string) {
+			if (!cmd->extra_string)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			else
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, ' ', cmd->extra_string);
+		} else {
 			packet_buf_write(&buf, "ng %s %s\n",
 					 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 4c3699fa85..119fd2fe59 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -726,9 +726,9 @@ test_expect_success "setup proc-receive hook" '
 	printf >&2 "# proc-receive hook\n"
 
 	test-tool proc-receive -v \
-		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
 		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
-		-r "$ZERO_OID $A refs/for/master/topic ok"
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
 	EOF
 	chmod a+x upstream/hooks/proc-receive
 '
@@ -752,17 +752,17 @@ test_expect_success "report update of all special refs" '
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To ../upstream
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/review/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
 	git -C upstream show-ref >out &&
@@ -791,6 +791,7 @@ test_expect_success "setup proc-receive hook" '
 # git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
 test_expect_success "report mixed refs update" '
 	git -C workbench push origin \
+		$B:refs/heads/master \
 		HEAD:refs/heads/bar \
 		HEAD:refs/heads/baz \
 		HEAD:refs/for/next/topic \
@@ -800,6 +801,7 @@ test_expect_success "report mixed refs update" '
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
@@ -811,12 +813,14 @@ test_expect_success "report mixed refs update" '
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
 	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To ../upstream
+	 <OID>..<OID> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
@@ -830,6 +834,80 @@ test_expect_success "report mixed refs update" '
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C upstream config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >upstream/hooks/proc-receive <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x upstream/hooks/proc-receive
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To ../upstream
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/master
 	EOF
 	test_cmp expect actual
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..cec3495d59 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
diff --git a/transport.c b/transport.c
index 272c0f4046..28731fa014 100644
--- a/transport.c
+++ b/transport.c
@@ -459,15 +459,18 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 	}
 }
 
-static void print_ref_status(char flag, const char *summary,
+static void print_ref_status(char flag, const char *summary, char *target_refname,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	if (!target_refname)
+		target_refname = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, target_refname);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, target_refname);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +484,9 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(target_refname));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(target_refname), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -495,18 +498,26 @@ static void print_ref_status(char flag, const char *summary,
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
 {
+	char *refname;
+
+	if (ref->remote_status && !strncmp(ref->remote_status, "ref:", 4))
+		refname = ref->remote_status + 4;
+	else
+		refname = ref->name;
+
 	if (ref->deletion)
-		print_ref_status('-', "[deleted]", ref, NULL, NULL,
+		print_ref_status('-', "[deleted]", refname, ref, NULL, NULL,
 				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+	else if (is_null_oid(&ref->old_oid)) {
+
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(refname, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(refname, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
-	else {
+				 refname, ref, ref->peer_ref, NULL, porcelain, summary_width);
+	} else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
@@ -525,7 +536,7 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
 					 DEFAULT_ABBREV);
 
-		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
+		print_ref_status(type, quickref.buf, refname, ref, ref->peer_ref, msg,
 				 porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
@@ -542,56 +553,56 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 
 	switch(ref->status) {
 	case REF_STATUS_NONE:
-		print_ref_status('X', "[no match]", ref, NULL, NULL,
+		print_ref_status('X', "[no match]", NULL, ref, NULL, NULL,
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
-		print_ref_status('!', "[rejected]", ref, NULL,
+		print_ref_status('!', "[rejected]", NULL, ref, NULL,
 				 "remote does not support deleting refs",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
-		print_ref_status('=', "[up to date]", ref,
+		print_ref_status('=', "[up to date]", NULL, ref,
 				 ref->peer_ref, NULL, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "non-fast-forward", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "already exists", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "fetch first", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "needs force", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "stale info", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "new shallow roots not allowed",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
-		print_ref_status('!', "[remote rejected]", ref,
+		print_ref_status('!', "[remote rejected]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 ref->remote_status, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
-		print_ref_status('!', "[remote failure]", ref,
+		print_ref_status('!', "[remote failure]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "atomic push failed", porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v5 6/6] doc: add documentation for the proc-receive hook
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (13 preceding siblings ...)
  2020-03-30 16:57 ` [PATCH v5 5/6] receive-pack: refactor report for proc-receive Jiang Xin
@ 2020-03-30 16:57 ` Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 0/7] New proc-receive hook for centralized workflow Jiang Xin
                   ` (7 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-03-30 16:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 70 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..10ea5c1f18 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,76 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  According to refnames of the commands which
+`git push` sends to 'git-receive-pack', the commands will be devided
+into two groups by matching what the `receive.procReceiveRefs`
+configuration variable defines.  One group of the commands will execute
+the internal `execute_commands` function to update the corresponding
+refnames, and the other group of commands which have matching refnames
+will execute this 'proc-receive' hook to create pull requests, etc.
+If there is no `receive.procReceiveRefs` settings, this hook won't
+execute at all, and all commands are sent to the internal
+`execute_commands` function.
+
+Its exit status only determines the success or failure of the group of
+commands with special refnames, unless atomic push is in use.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but will talk a protocol in pkt-line format with the
+'receive-pack' for reading commands, push-options (optional), and
+sending result.  In the following example, The letter "S" stands for
+"receive-pack" and letter "H" stands for the hook.
+
+    # Version and capabilities negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(old-oid new-oid ref)
+    S: ... ...
+    S: flush-pkt
+    # Only if push-options have been negotiated.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(old-oid new-oid ref ok)
+    # NO, I reject it.
+    H: PKT-LINE(old-oid new-oid ref ng reason)
+    # OK, but use an alternate reference.
+    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
+    # It will fallthrough to receive-pack to execute.
+    H: PKT-LINE(old-oid new-oid ref ft)
+    H: ... ...
+    H: flush-pkt
+
+The "proc-receive" hook may update one or more references, and will send
+its result one by one in pkt-line format.  Each line of the result has
+four fields and one optional message field, like "<old-oid> <new-oid>
+<ref> <status> [<message>]".
+
+The first three fields are the same as those of the commands for
+"receive-pack".
+
+The forth field has a two-letter status code.  Available status codes:
+
+* ok: The command runs successfully.  If the optional message has a
+  prefix "ref:", the hook has created/updated an alternate reference
+  instead.
+
+* ng: Fail to run the command.  Error message is given in the optional
+  message field.
+
+* ft: Will fallthrough to receive-pack to execute.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.26.0.4.g39bcdcb101.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v5 2/6] receive-pack: add new proc-receive hook
  2020-03-30 16:57 ` [PATCH v5 2/6] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-03-31  0:19   ` Junio C Hamano
  2020-03-31  0:21   ` Junio C Hamano
  1 sibling, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-03-31  0:19 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> +	argv[0] = find_hook("proc-receive");
> +	if (!argv[0]) {
> +		rp_error("cannot to find hook 'proc-receive'");

Perhaps discard "to" from here.  Also I notice that other messages
given to rp_error() are marked for _(i18n), but this one is not.

> +	make_user_friendly_and_stable_output <out >actual &&
> +	cat >expect <<-EOF &&
> +	remote: # pre-receive hook
> +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
> +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
> +	remote: error: cannot to find hook "proc-receive"
> +	remote: # post-receive hook
> +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
> +	To ../upstream
> +	 * [new branch] HEAD -> next
> +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
> +	error: failed to push some refs to "../upstream"
> +	EOF
> +	test_cmp expect actual &&

This breaks gettext-poison test.  

See https://travis-ci.org/github/git/git/jobs/669008025 for example.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v5 2/6] receive-pack: add new proc-receive hook
  2020-03-30 16:57 ` [PATCH v5 2/6] receive-pack: add new proc-receive hook Jiang Xin
  2020-03-31  0:19   ` Junio C Hamano
@ 2020-03-31  0:21   ` Junio C Hamano
  1 sibling, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-03-31  0:21 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
> new file mode 100644
> index 0000000000..aafa3e8e59
> --- /dev/null
> +++ b/t/helper/test-proc-receive.c
> @@ -0,0 +1,172 @@
> +static void proc_receive_verison(struct packet_reader *reader) {

Is that a phone company ;-)?

> +	int server_version = 0;
> +
> +	for (;;) {
> +...
> +static void proc_receive_read_commands(struct packet_reader *reader,
> +				       struct command **commands)
> +{
> +	struct command **tail = commands;
> +
> +	for (;;) {
> +		struct object_id old_oid, new_oid;
> +		struct command *cmd;
> +		const char *refname;
> +		const char *p;
> +
> +		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
> +			break;
> +		}
> +
> +		if (parse_oid_hex(reader->line, &old_oid, &p) ||
> +		    *p++ != ' ' ||
> +		    parse_oid_hex(p, &new_oid, &p) ||
> +		    *p++ != ' ')
> +			die("protocol error: expected 'old new ref', got '%s'",
> +			    reader->line);
> +		refname = p;
> +		FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname));

		FLEX_ALLOC_STR(cmd, ref_name, refname);

> +		oidcpy(&cmd->old_oid, &old_oid);
> +		oidcpy(&cmd->new_oid, &new_oid);
> +
> +		*tail = cmd;
> +		tail = &cmd->next;
> +	}
> +}

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v6 0/7] New proc-receive hook for centralized workflow
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (14 preceding siblings ...)
  2020-03-30 16:57 ` [PATCH v5 6/6] doc: add documentation for the proc-receive hook Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  2020-04-02 18:26   ` Junio C Hamano
  2020-04-02 16:35 ` [PATCH v6 1/7] transport: not report a non-head push as a branch Jiang Xin
                   ` (6 subsequent siblings)
  22 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v5

+ Fixed code review issues on v5.

  * s/cannot to/cannot/

  * s/FLEX_ALLOC_MEM/FLEX_ALLOC_STR/

  * Add flag `C_LOCALE_OUTPUT` on test cases which may break
    gettext-poison test.

  * Note: `rp_errors()` in "receive-pack" is executed on the
    server side, I think leave them unmarked for i18n is better.

+ Add t5412 for "proc-receive" hook test on HTTP protocol.

+ Some test cases of t5412 will hang because of "receive-pack" die
  earlier.  Replace `die()` with `rp_error()`.


## Range-diff v5...v6

1:  a8bcc20439 = 1:  1647024dfe transport: not report a non-head push as a branch
2:  14641ec57e ! 2:  ed8d2c0e82 receive-pack: add new proc-receive hook
    @@ builtin/receive-pack.c: static void write_head_info(void)
      	packet_flush(1);
      }
      
    -+#define RUN_PROC_RECEIVE_SCHEDULE	1
    ++#define RUN_PROC_RECEIVE_SCHEDULED	1
     +#define RUN_PROC_RECEIVE_RETURNED	2
      struct command {
      	struct command *next;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		if (parse_oid_hex(reader->line, &old_oid, &p) ||
     +		    *p++ != ' ' ||
     +		    parse_oid_hex(p, &new_oid, &p) ||
    -+		    *p++ != ' ')
    -+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
    -+			    reader->line);
    ++		    *p++ != ' ') {
    ++			rp_error("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
    ++				 reader->line);
    ++			return -1;
    ++		}
     +
     +		refname = p;
     +		status = strchr(p, ' ');
    -+		if (!status)
    -+			die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
    -+			    reader->line);
    ++		if (!status) {
    ++			rp_error("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
    ++				 reader->line);
    ++			return -1;
    ++		}
     +		*status++ = '\0';
     +		if (strlen(status) > 2 && *(status + 2) == ' ') {
     +			msg = status + 2;
     +			*msg++ = '\0';
     +		}
    -+		if (strlen(status) != 2)
    -+			die("protocol error: proc-receive has bad status '%s' for '%s'",
    -+			    status, reader->line);
    ++		if (strlen(status) != 2) {
    ++			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
    ++				 status, reader->line);
    ++			return -1;
    ++		}
     +
     +		/* first try searching at our hint, falling back to all refs */
     +		if (hint)
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +				hint->error_string = "failed";
     +			code = 1;
     +		} else if (strcmp("ok", status)) {
    -+			die("protocol error: proc-receive has bad status '%s' for '%s'",
    -+			    status, reader->line);
    ++			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
    ++				 status, reader->line);
    ++			return -1;
     +		}
     +	}
     +
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +	argv[0] = find_hook("proc-receive");
     +	if (!argv[0]) {
    -+		rp_error("cannot to find hook 'proc-receive'");
    -+		return 1;
    ++		rp_error("cannot find hook 'proc-receive'");
    ++		return -1;
     +	}
     +	argv[1] = NULL;
     +
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		}
     +	}
     +
    -+	if (version != 1)
    -+		die("protocol error: unknown proc-receive version '%d'", version);
    ++	if (version != 1) {
    ++		rp_error("protocol error: unknown proc-receive version '%d'",
    ++			 version);
    ++		code = -1;
    ++		goto cleanup;
    ++	}
     +
     +	/* Send commands */
     +	for (cmd = commands; cmd; cmd = cmd->next) {
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
     +			continue;
    -+
     +		old_hex = oid_to_hex(&cmd->old_oid);
     +		new_hex = oid_to_hex(&cmd->new_oid);
    -+
     +		packet_write_fmt(proc.in, "%s %s %s",
     +				 old_hex, new_hex, cmd->ref_name);
     +	}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +		for_each_string_list_item(item, push_options)
     +			packet_write_fmt(proc.in, "%s", item->string);
    -+
     +		packet_flush(proc.in);
     +	}
     +
     +	/* Read result from proc-receive */
     +	code = read_proc_receive_result(&reader, commands);
    ++
    ++cleanup:
     +	close(proc.in);
     +	close(proc.out);
     +	if (use_sideband)
     +		finish_async(&muxer);
    -+	if (finish_command(&proc))
    -+		die("proc-receive did not exit properly");
    -+
    ++	if (finish_command(&proc)) {
    ++		rp_error("proc-receive did not exit properly");
    ++		code = -1;
    ++	}
     +	sigchain_pop(SIGPIPE);
    -+
     +	return code;
     +}
     +
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +
     +		/* TODO: replace the fixed prefix by looking up git config variables. */
     +		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
    -+			cmd->run_proc_receive = 1;
    ++			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     +			run_proc_receive = 1;
     +		}
     +	}
    @@ t/helper/test-proc-receive.c (new)
     +			die("protocol error: expected 'old new ref', got '%s'",
     +			    reader->line);
     +		refname = p;
    -+		FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname));
    ++		FLEX_ALLOC_STR(cmd, ref_name, refname);
     +		oidcpy(&cmd->old_oid, &old_oid);
     +		oidcpy(&cmd->new_oid, &new_oid);
     +
    @@ t/helper/test-tool.h: int cmd__parse_pathspec_file(int argc, const char** argv);
      int cmd__read_cache(int argc, const char **argv);
     
      ## t/t5411-proc-receive-hook.sh ##
    +@@ t/t5411-proc-receive-hook.sh: create_commits_in () {
    + # Format the output of git-push, git-show-ref and other commands to make a
    + # user-friendly and stable text.  We can easily prepare the expect text
    + # without having to worry about future changes of the commit ID and spaces
    +-# of the output.  We also replce single quotes with double quotes, because
    +-# it is boring to prepare unquoted single quotes in expect txt.
    ++# of the output.  Single quotes are replaced with double quotes, because
    ++# it is boring to prepare unquoted single quotes in expect txt.  We also
    ++# remove some locale error messages, which break test if we turn on
    ++# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
    ++# on plumbing commands.
    + make_user_friendly_and_stable_output () {
    + 	sed \
    + 		-e "s/  *\$//" \
     @@ t/t5411-proc-receive-hook.sh: make_user_friendly_and_stable_output () {
    - 		-e "s/[0-9a-f]\{7,\}/<OID>/g"
    - }
    - 
    + 		-e "s/$B/<COMMIT-B>/g" \
    + 		-e "s/$TAG/<TAG-v123>/g" \
    + 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
    +-		-e "s/[0-9a-f]\{7,\}/<OID>/g"
    ++		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
    ++		-e "s#To ../upstream.git#To <URL/of/upstream.git>#" \
    ++		-e "/^error: / d"
    ++}
    ++
     +# Asynchronous sideband may generate inconsistent output messages,
     +# sort before comparison.
     +test_sorted_cmp () {
    @@ t/t5411-proc-receive-hook.sh: make_user_friendly_and_stable_output () {
     +			$GIT_TEST_CMP "$@"
     +		fi
     +	fi
    -+}
    -+
    + }
    + 
      # Refs of upstream : master(B)  next(A)
      # Refs of workbench: master(A)           tags/v123
      test_expect_success "setup" '
    +-	git init --bare upstream &&
    ++	upstream=upstream.git &&
    ++	git init --bare "$upstream" &&
    + 	git init workbench &&
    + 	create_commits_in workbench A B &&
    + 	(
    +@@ t/t5411-proc-receive-hook.sh: test_expect_success "setup" '
    + 		# Try to make a stable fixed width for abbreviated commit ID,
    + 		# this fixed-width oid will be replaced with "<OID>".
    + 		git config core.abbrev 7 &&
    +-		git remote add origin ../upstream &&
    ++		git remote add origin ../$upstream &&
    + 		git update-ref refs/heads/master $A &&
    + 		git tag -m "v123" v123 $A &&
    + 		git push origin \
    +@@ t/t5411-proc-receive-hook.sh: test_expect_success "setup" '
    + 	TAG=$(git -C workbench rev-parse v123) &&
    + 
    + 	# setup pre-receive hook
    +-	cat >upstream/hooks/pre-receive <<-\EOF &&
    ++	cat >"$upstream/hooks/pre-receive" <<-\EOF &&
    + 	#!/bin/sh
    + 
    + 	echo >&2 "# pre-receive hook"
    +@@ t/t5411-proc-receive-hook.sh: test_expect_success "setup" '
    + 	EOF
    + 
    + 	# setup post-receive hook
    +-	cat >upstream/hooks/post-receive <<-\EOF &&
    ++	cat >"$upstream/hooks/post-receive" <<-\EOF &&
    + 	#!/bin/sh
    + 
    + 	echo >&2 "# post-receive hook"
    +@@ t/t5411-proc-receive-hook.sh: test_expect_success "setup" '
    + 	EOF
    + 
    + 	chmod a+x \
    +-		upstream/hooks/pre-receive \
    +-		upstream/hooks/post-receive
    ++		"$upstream/hooks/pre-receive" \
    ++		"$upstream/hooks/post-receive"
    + '
    + 
    + # Refs of upstream : master(B)  next(A)
    +@@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
    + 	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
    +-	To ../upstream
    ++	To <URL/of/upstream.git>
    + 	 + <OID>...<OID> HEAD -> master (forced update)
    + 	 - [deleted] next
    + 	 * [new tag] v123 -> v123
    +@@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
    + 	 * [new branch] HEAD -> a/b/c
    + 	EOF
    + 	test_cmp expect actual &&
    +-	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
    + 	make_user_friendly_and_stable_output <out >actual &&
    + 	cat >expect <<-EOF &&
    + 	<COMMIT-A> refs/heads/a/b/c
     @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
      	test_cmp expect actual
      '
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
     +	(
    -+		cd upstream &&
    ++		cd "$upstream" &&
     +		git update-ref -d refs/review/master/topic &&
     +		git update-ref -d refs/tags/v123 &&
     +		git update-ref -d refs/heads/a/b/c
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: error: cannot to find hook "proc-receive"
    ++	remote: error: cannot find hook "proc-receive"
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 * [new branch] HEAD -> next
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
    -+	error: failed to push some refs to "../upstream"
     +	EOF
    -+	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)             next(A)
     +# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
    -+	git -C upstream update-ref -d refs/heads/next
    ++	git -C "$upstream" update-ref -d refs/heads/next
     +'
     +
     +# Refs of upstream : master(A)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: error: cannot to find hook "proc-receive"
    -+	To ../upstream
    ++	remote: error: cannot find hook "proc-receive"
    ++	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
    -+	error: failed to push some refs to "../upstream"
     +	EOF
    -+	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (bad version)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF &&
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF &&
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
     +
     +	test-tool proc-receive -v --version 2
     +	EOF
    -+	chmod a+x upstream/hooks/proc-receive
    ++	chmod a+x "$upstream/hooks/proc-receive"
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive bad protocol: unknown version" '
    ++test_expect_success C_LOCALE_OUTPUT "proc-receive bad protocol: unknown version" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	fatal: protocol error: unknown proc-receive version "2"
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: error: protocol error: unknown proc-receive version "2"
    ++	remote: fatal: the remote end hung up unexpectedly
    ++	remote: error: proc-receive did not exit properly
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (no report)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 * [new branch] HEAD -> next
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    -+	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)             next(A)
     +# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
    -+	git -C upstream update-ref -d refs/heads/next
    ++	git -C "$upstream" update-ref -d refs/heads/next
     +
     +'
     +
     +test_expect_success "setup proc-receive hook (bad oid)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic\
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> bad-id new-id ref ok
    ++	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (no status)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	fatal: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (unknown status)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	test_must_fail git -C workbench push origin \
     +			HEAD:refs/for/master/topic \
     +			>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	fatal: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
    ++	remote: error: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (bad status)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out | grep "protocol error" >actual &&
    ++	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	fatal: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
    ++	remote: error: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	test_sorted_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (ng)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
    -+	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (ng message)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
    -+	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (report status on builtin command)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	warning: proc-receive reported status on ref of builtin command: refs/heads/master
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 <OID>..<OID> <COMMIT-B> -> master
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    -+	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_sorted_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-B> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "cleanup" '
    -+	git -C upstream update-ref refs/heads/master $A
    ++	git -C "$upstream" update-ref refs/heads/master $A
     +'
     +
     +test_expect_success "setup proc-receive hook (ok)" '
    -+	cat >upstream/hooks/proc-receive <<-EOF
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
     +	warning: proc-receive reported status on unknown ref: refs/for/master/topic
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
    -+	error: failed to push some refs to "../upstream"
     +	EOF
     +	test_sorted_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -o ...  :                       refs/for/master/topic
    -+test_expect_success "not support push options" '
    ++test_expect_success C_LOCALE_OUTPUT "not support push options" '
     +	test_must_fail git -C workbench push \
     +		-o issue=123 \
     +		-o reviewer=user1 \
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	fatal: the remote end hung up unexpectedly
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "enable push options" '
    -+	git -C upstream config receive.advertisePushOptions true
    ++	git -C "$upstream" config receive.advertisePushOptions true
     +'
     +
     +# Refs of upstream : master(A)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 * [new branch] HEAD -> next
     +	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
3:  2440a474bd = 3:  1aa84fdf6f refs.c: refactor to reuse ref_is_hidden()
4:  e041582643 ! 4:  a301edb86c receive-pack: new config receive.procReceiveRefs
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      
     -		/* TODO: replace the fixed prefix by looking up git config variables. */
     -		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
    --			cmd->run_proc_receive = 1;
    +-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     -			run_proc_receive = 1;
     +		for (cmd = commands; cmd; cmd = cmd->next) {
     +			if (!should_process_cmd(cmd))
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +			strbuf_setlen(&refname_full, prefix_len);
     +			strbuf_addstr(&refname_full, cmd->ref_name);
     +			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
    -+				cmd->run_proc_receive = 1;
    ++				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     +				run_proc_receive = 1;
     +			}
      		}
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "cleanup" '
      
     +test_expect_success "add two receive.procReceiveRefs settings" '
     +	(
    -+		cd upstream &&
    ++		cd "$upstream" &&
     +		git config --add receive.procReceiveRefs refs/for/ &&
     +		git config --add receive.procReceiveRefs refs/review/
     +	)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +# Refs of upstream : master(A)             next(A)
     +# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
    -+	git -C upstream update-ref -d refs/heads/next
    ++	git -C "$upstream" update-ref -d refs/heads/next
     +'
     +
     +test_expect_success "setup proc-receive hook" '
    -+	cat >upstream/hooks/proc-receive <<-EOF &&
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF &&
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
     +
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/next/topic ok" \
     +		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
    ++		-r "$ZERO_OID $A refs/for/next/topic ok" \
     +		-r "$ZERO_OID $A refs/for/master/topic ok"
     +	EOF
    -+	chmod a+x upstream/hooks/proc-receive
    ++	chmod a+x "$upstream/hooks/proc-receive"
     +'
     +
     +# Refs of upstream : master(A)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 * [new reference] HEAD -> refs/for/next/topic
     +	 * [new reference] HEAD -> refs/review/a/b/c/topic
     +	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +'
     +
     +test_expect_success "setup proc-receive hook" '
    -+	cat >upstream/hooks/proc-receive <<-EOF &&
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF &&
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +		-r "$ZERO_OID $A refs/for/next/topic ok" \
     +		-r "$ZERO_OID $A refs/for/master/topic ok"
     +	EOF
    -+	chmod a+x upstream/hooks/proc-receive
    ++	chmod a+x "$upstream/hooks/proc-receive"
     +'
     +
     +# Refs of upstream : master(A)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 * [new branch] HEAD -> bar
     +	 * [new branch] HEAD -> baz
     +	 * [new reference] HEAD -> refs/for/next/topic
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "push with options" '
     +	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/bar
5:  439acbdb62 ! 5:  538fa71f48 receive-pack: refactor report for proc-receive
    @@ builtin/receive-pack.c: static int read_proc_receive_result(struct packet_reader
     +			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
     +			hint->run_proc_receive = 0;
     +		} else {
    - 			die("protocol error: proc-receive has bad status '%s' for '%s'",
    - 			    status, reader->line);
    - 		}
    + 			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
    + 				 status, reader->line);
    + 			return -1;
     @@ builtin/receive-pack.c: static void report(struct command *commands, const char *unpack_status)
      	packet_buf_write(&buf, "unpack %s\n",
      			 unpack_status ? unpack_status : "ok");
    @@ builtin/receive-pack.c: static void report(struct command *commands, const char
     
      ## t/t5411-proc-receive-hook.sh ##
     @@ t/t5411-proc-receive-hook.sh: test_expect_success "setup proc-receive hook" '
    - 	printf >&2 "# proc-receive hook\n"
      
      	test-tool proc-receive -v \
    --		-r "$ZERO_OID $A refs/for/next/topic ok" \
    -+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
      		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
    +-		-r "$ZERO_OID $A refs/for/next/topic ok" \
     -		-r "$ZERO_OID $A refs/for/master/topic ok"
    ++		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
     +		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
      	EOF
    - 	chmod a+x upstream/hooks/proc-receive
    + 	chmod a+x "$upstream/hooks/proc-receive"
      '
     @@ t/t5411-proc-receive-hook.sh: test_expect_success "report update of all special refs" '
    - 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
      	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
      	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    --	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
      	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
    +-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
     -	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
      	remote: # post-receive hook
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    - 	To ../upstream
    + 	To <URL/of/upstream.git>
     -	 * [new reference] HEAD -> refs/for/next/topic
     +	 * [new reference] HEAD -> refs/pull/123/head
      	 * [new reference] HEAD -> refs/review/a/b/c/topic
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "report update of all special
     +	 * [new reference] HEAD -> refs/pull/124/head
      	EOF
      	test_cmp expect actual &&
    - 	git -C upstream show-ref >out &&
    + 	git -C "$upstream" show-ref >out &&
     @@ t/t5411-proc-receive-hook.sh: test_expect_success "setup proc-receive hook" '
      # git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
      test_expect_success "report mixed refs update" '
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "report mixed refs update" '
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    - 	To ../upstream
    + 	To <URL/of/upstream.git>
     +	 <OID>..<OID> <COMMIT-B> -> master
      	 * [new branch] HEAD -> bar
      	 * [new branch] HEAD -> baz
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "report mixed refs update" '
     +'
     +
     +test_expect_success "config receive.procReceiveRefs for all ref/" '
    -+	git -C upstream config --add receive.procReceiveRefs refs/
    ++	git -C "$upstream" config --add receive.procReceiveRefs refs/
     +'
     +
     +test_expect_success "setup proc-receive hook" '
    -+	cat >upstream/hooks/proc-receive <<-EOF &&
    ++	cat >"$upstream/hooks/proc-receive" <<-EOF &&
     +	#!/bin/sh
     +
     +	printf >&2 "# proc-receive hook\n"
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "report mixed refs update" '
     +		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
     +		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
     +	EOF
    -+	chmod a+x upstream/hooks/proc-receive
    ++	chmod a+x "$upstream/hooks/proc-receive"
     +'
     +
     +# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "report mixed refs update" '
     +	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
     +	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
    -+	To ../upstream
    ++	To <URL/of/upstream.git>
     +	 <OID>..<OID> <COMMIT-B> -> bar
     +	 - [deleted] foo
     +	 + <OID>...<OID> HEAD -> master (forced update)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "report mixed refs update" '
     +	 * [new reference] HEAD -> refs/pull/124/head
     +	EOF
     +	test_cmp expect actual &&
    -+	git -C upstream show-ref >out &&
    ++	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-B> refs/heads/bar
-:  ---------- > 6:  20289547f3 t5412: test proc-receive hook on HTTP protocol
6:  24e5cddee0 = 7:  da9106a42d doc: add documentation for the proc-receive hook


Jiang Xin (7):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  receive-pack: refactor report for proc-receive
  t5412: test proc-receive hook on HTTP protocol
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt           |  14 +
 Documentation/githooks.txt                 |  70 ++
 Makefile                                   |   1 +
 builtin/receive-pack.c                     | 300 ++++++-
 refs.c                                     |  11 +-
 refs.h                                     |   1 +
 t/helper/test-proc-receive.c               | 172 ++++
 t/helper/test-tool.c                       |   1 +
 t/helper/test-tool.h                       |   1 +
 t/t5411-proc-receive-hook.sh               | 950 ++++++++++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh | 960 +++++++++++++++++++++
 t/t5516-fetch-push.sh                      |   2 +-
 transport-helper.c                         |  64 +-
 transport.c                                |  62 +-
 14 files changed, 2543 insertions(+), 66 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v6 1/7] transport: not report a non-head push as a branch
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (15 preceding siblings ...)
  2020-04-02 16:35 ` [PATCH v6 0/7] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 2/7] receive-pack: add new proc-receive hook Jiang Xin
                   ` (5 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh | 153 +++++++++++++++++++++++++++++++++++
 t/t5516-fetch-push.sh        |   2 +-
 transport.c                  |   9 ++-
 3 files changed, 160 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..1784bcb584
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,153 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To ../upstream
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..272c0f4046 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v6 2/7] receive-pack: add new proc-receive hook
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (16 preceding siblings ...)
  2020-04-02 16:35 ` [PATCH v6 1/7] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                   ` (4 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and capabilities negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt
    # Only if push-options have been negotiated.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(old-oid new-oid ref ok)
    # NO, I reject it.
    H: PKT-LINE(old-oid new-oid ref ng reason)
    # OK, but use an alternate reference. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
    # It will fallthrough to receive-pack to execute. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ft)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook can create/update another alternate
reference.  For example, a command for a reference "refs/for/master" may
create a special reference, such as "refs/pull/123/head".  The alternate
reference can be returned from the result in an extensible format like
"<old-oid> <new-oid> <reference> <status> [<message>]".

The result will be stored in a command list, and "receive-pack" will use
the result to replace the commands that have specific `run_proc_receive`
field turned on.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 252 ++++++++++++++-
 t/helper/test-proc-receive.c | 172 ++++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411-proc-receive-hook.sh | 611 ++++++++++++++++++++++++++++++++++-
 6 files changed, 1024 insertions(+), 14 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..45f2d426fc 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +820,222 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for ( ; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_result(struct packet_reader *reader,
+				    struct command *commands)
+{
+	struct command *hint;
+	struct command *cmd;
+	int code = 0;
+
+	hint = NULL;
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ') {
+			rp_error("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+				 reader->line);
+			return -1;
+		}
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status) {
+			rp_error("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
+				 reader->line);
+			return -1;
+		}
+		*status++ = '\0';
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2) {
+			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
+				 status, reader->line);
+			return -1;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			warning("proc-receive reported status on unknown ref: %s",
+				refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			warning("proc-receive reported status on ref of builtin command: %s",
+				refname);
+			continue;
+		}
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		oidcpy(&hint->old_oid, &old_oid);
+		oidcpy(&hint->new_oid, &new_oid);
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				hint->error_string = xstrdup(msg);
+			else
+				hint->error_string = "failed";
+			code = 1;
+		} else if (strcmp("ok", status)) {
+			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
+				 status, reader->line);
+			return -1;
+		}
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED))
+		    cmd->error_string = "no report from proc-receive";
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		rp_error("protocol error: unknown proc-receive version '%d'",
+			 version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		char *old_hex, *new_hex;
+
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		old_hex = oid_to_hex(&cmd->old_oid);
+		new_hex = oid_to_hex(&cmd->new_oid);
+		packet_write_fmt(proc.in, "%s %s %s",
+				 old_hex, new_hex, cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_result(&reader, commands);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc)) {
+		rp_error("proc-receive did not exit properly");
+		code = -1;
+	}
+	sigchain_pop(SIGPIPE);
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1611,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1651,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1687,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1717,20 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1757,18 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive) {
+		int code;
+
+		code = run_proc_receive_hook(commands, push_options);
+		if (code) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+					cmd->error_string = "fail to run proc-receive hook";
+			}
+		}
+	}
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..ed30817905
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,172 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			char *old_hex, *new_hex;
+
+			old_hex = oid_to_hex(&cmd->old_oid);
+			new_hex = oid_to_hex(&cmd->new_oid);
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				old_hex, new_hex, cmd->ref_name);
+		}
+
+		if (push_options.nr > 0) {
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+		}
+
+		if (returns.nr) {
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+		}
+	}
+
+	if (returns.nr) {
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	}
+	packet_flush(1);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 1784bcb584..a17fd9814c 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -43,8 +43,11 @@ create_commits_in () {
 # Format the output of git-push, git-show-ref and other commands to make a
 # user-friendly and stable text.  We can easily prepare the expect text
 # without having to worry about future changes of the commit ID and spaces
-# of the output.  We also replce single quotes with double quotes, because
-# it is boring to prepare unquoted single quotes in expect txt.
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
 make_user_friendly_and_stable_output () {
 	sed \
 		-e "s/  *\$//" \
@@ -54,13 +57,34 @@ make_user_friendly_and_stable_output () {
 		-e "s/$B/<COMMIT-B>/g" \
 		-e "s/$TAG/<TAG-v123>/g" \
 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
-		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
+		-e "s#To ../upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
+
+# Asynchronous sideband may generate inconsistent output messages,
+# sort before comparison.
+test_sorted_cmp () {
+	if ! $GIT_TEST_CMP "$@" >/dev/null 2>&1
+	then
+		cmd=$GIT_TEST_CMP
+		for f in "$@"
+		do
+			sort "$f" >"$f.sorted"
+			cmd="$cmd \"$f.sorted\""
+		done
+		if ! eval $cmd >/dev/null 2>&1
+		then
+			$GIT_TEST_CMP "$@"
+		fi
+	fi
 }
 
 # Refs of upstream : master(B)  next(A)
 # Refs of workbench: master(A)           tags/v123
 test_expect_success "setup" '
-	git init --bare upstream &&
+	upstream=upstream.git &&
+	git init --bare "$upstream" &&
 	git init workbench &&
 	create_commits_in workbench A B &&
 	(
@@ -68,7 +92,7 @@ test_expect_success "setup" '
 		# Try to make a stable fixed width for abbreviated commit ID,
 		# this fixed-width oid will be replaced with "<OID>".
 		git config core.abbrev 7 &&
-		git remote add origin ../upstream &&
+		git remote add origin ../$upstream &&
 		git update-ref refs/heads/master $A &&
 		git tag -m "v123" v123 $A &&
 		git push origin \
@@ -78,7 +102,7 @@ test_expect_success "setup" '
 	TAG=$(git -C workbench rev-parse v123) &&
 
 	# setup pre-receive hook
-	cat >upstream/hooks/pre-receive <<-\EOF &&
+	cat >"$upstream/hooks/pre-receive" <<-\EOF &&
 	#!/bin/sh
 
 	echo >&2 "# pre-receive hook"
@@ -90,7 +114,7 @@ test_expect_success "setup" '
 	EOF
 
 	# setup post-receive hook
-	cat >upstream/hooks/post-receive <<-\EOF &&
+	cat >"$upstream/hooks/post-receive" <<-\EOF &&
 	#!/bin/sh
 
 	echo >&2 "# post-receive hook"
@@ -102,8 +126,8 @@ test_expect_success "setup" '
 	EOF
 
 	chmod a+x \
-		upstream/hooks/pre-receive \
-		upstream/hooks/post-receive
+		"$upstream/hooks/pre-receive" \
+		"$upstream/hooks/post-receive"
 '
 
 # Refs of upstream : master(B)  next(A)
@@ -131,7 +155,7 @@ test_expect_success "normal git-push command" '
 	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
-	To ../upstream
+	To <URL/of/upstream.git>
 	 + <OID>...<OID> HEAD -> master (forced update)
 	 - [deleted] next
 	 * [new tag] v123 -> v123
@@ -139,7 +163,7 @@ test_expect_success "normal git-push command" '
 	 * [new branch] HEAD -> a/b/c
 	EOF
 	test_cmp expect actual &&
-	git -C upstream show-ref >out &&
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	<COMMIT-A> refs/heads/a/b/c
@@ -150,4 +174,569 @@ test_expect_success "normal git-push command" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic:                       next(A)  refs/for/master/topic(A)
+test_expect_failure "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success C_LOCALE_OUTPUT "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: error: protocol error: unknown proc-receive version "2"
+	remote: fatal: the remote end hung up unexpectedly
+	remote: error: proc-receive did not exit properly
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	warning: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	warning: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success C_LOCALE_OUTPUT "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v6 3/7] refs.c: refactor to reuse ref_is_hidden()
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (17 preceding siblings ...)
  2020-04-02 16:35 ` [PATCH v6 2/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
                   ` (3 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v6 4/7] receive-pack: new config receive.procReceiveRefs
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (18 preceding siblings ...)
  2020-04-02 16:35 ` [PATCH v6 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 5/7] receive-pack: refactor report for proc-receive Jiang Xin
                   ` (2 subsequent siblings)
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 ++++
 builtin/receive-pack.c           |  43 ++++++++--
 t/t5411-proc-receive-hook.sh     | 130 +++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..0178f2d478 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for/", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 45f2d426fc..3b2f949255 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1720,15 +1735,26 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2188,6 +2214,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2303,5 +2331,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index a17fd9814c..6a4eaf2d8a 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -185,6 +185,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for/ &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -739,4 +747,126 @@ test_expect_success "push with options" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v6 5/7] receive-pack: refactor report for proc-receive
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (19 preceding siblings ...)
  2020-04-02 16:35 ` [PATCH v6 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 6/7] t5412: test proc-receive hook on HTTP protocol Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The "proc-receive" may update one or more references, and will send its
result one by one in pkt-line format.  Each line of the result has four
fields and one optional message field, as "<old-oid> <new-oid> <ref>
<status> [<message>]".  See the following example:

    # OK, run this command successfully.
    PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference.
    PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute.
    PKT-LINE(old-oid new-oid ref ft)

The first three fields have the same foramt as a command.

The forth field has a two-letter status code.  Available status code:

* ok: The command runs successfully.  If the optional message has a
  prefix "ref:", the hook has created/updated an alternate reference
  instead.

* ng: Fail to run the command. Error message is in the optional message
  field.

* ft: Will fallthrough to receive-pack to execute.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 21 +++++++--
 t/t5411-proc-receive-hook.sh | 90 +++++++++++++++++++++++++++++++++---
 transport-helper.c           | 64 ++++++++++++-------------
 transport.c                  | 59 +++++++++++++----------
 4 files changed, 168 insertions(+), 66 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 3b2f949255..5c403993a9 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -328,6 +328,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extra_string;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     run_proc_receive:2;
@@ -912,7 +913,12 @@ static int read_proc_receive_result(struct packet_reader *reader,
 			else
 				hint->error_string = "failed";
 			code = 1;
-		} else if (strcmp("ok", status)) {
+		} else if (!strcmp("ok", status)) {
+			hint->extra_string = xstrdup_or_null(msg);
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else {
 			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
 				 status, reader->line);
 			return -1;
@@ -2171,12 +2177,17 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
+		if (!cmd->error_string) {
+			if (!cmd->extra_string)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			else
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, ' ', cmd->extra_string);
+		} else {
 			packet_buf_write(&buf, "ng %s %s\n",
 					 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 6a4eaf2d8a..2b100d7edc 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -761,8 +761,8 @@ test_expect_success "setup proc-receive hook" '
 
 	test-tool proc-receive -v \
 		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
-		-r "$ZERO_OID $A refs/for/next/topic ok" \
-		-r "$ZERO_OID $A refs/for/master/topic ok"
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
 	EOF
 	chmod a+x "$upstream/hooks/proc-receive"
 '
@@ -787,16 +787,16 @@ test_expect_success "report update of all special refs" '
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/review/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -825,6 +825,7 @@ test_expect_success "setup proc-receive hook" '
 # git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
 test_expect_success "report mixed refs update" '
 	git -C workbench push origin \
+		$B:refs/heads/master \
 		HEAD:refs/heads/bar \
 		HEAD:refs/heads/baz \
 		HEAD:refs/for/next/topic \
@@ -834,6 +835,7 @@ test_expect_success "report mixed refs update" '
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
@@ -845,12 +847,14 @@ test_expect_success "report mixed refs update" '
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
 	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
@@ -864,6 +868,80 @@ test_expect_success "report mixed refs update" '
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/master
 	EOF
 	test_cmp expect actual
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..cec3495d59 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
diff --git a/transport.c b/transport.c
index 272c0f4046..28731fa014 100644
--- a/transport.c
+++ b/transport.c
@@ -459,15 +459,18 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 	}
 }
 
-static void print_ref_status(char flag, const char *summary,
+static void print_ref_status(char flag, const char *summary, char *target_refname,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	if (!target_refname)
+		target_refname = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, target_refname);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, target_refname);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +484,9 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(target_refname));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(target_refname), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -495,18 +498,26 @@ static void print_ref_status(char flag, const char *summary,
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
 {
+	char *refname;
+
+	if (ref->remote_status && !strncmp(ref->remote_status, "ref:", 4))
+		refname = ref->remote_status + 4;
+	else
+		refname = ref->name;
+
 	if (ref->deletion)
-		print_ref_status('-', "[deleted]", ref, NULL, NULL,
+		print_ref_status('-', "[deleted]", refname, ref, NULL, NULL,
 				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+	else if (is_null_oid(&ref->old_oid)) {
+
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(refname, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(refname, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
-	else {
+				 refname, ref, ref->peer_ref, NULL, porcelain, summary_width);
+	} else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
@@ -525,7 +536,7 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
 					 DEFAULT_ABBREV);
 
-		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
+		print_ref_status(type, quickref.buf, refname, ref, ref->peer_ref, msg,
 				 porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
@@ -542,56 +553,56 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 
 	switch(ref->status) {
 	case REF_STATUS_NONE:
-		print_ref_status('X', "[no match]", ref, NULL, NULL,
+		print_ref_status('X', "[no match]", NULL, ref, NULL, NULL,
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
-		print_ref_status('!', "[rejected]", ref, NULL,
+		print_ref_status('!', "[rejected]", NULL, ref, NULL,
 				 "remote does not support deleting refs",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
-		print_ref_status('=', "[up to date]", ref,
+		print_ref_status('=', "[up to date]", NULL, ref,
 				 ref->peer_ref, NULL, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "non-fast-forward", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "already exists", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "fetch first", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "needs force", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "stale info", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "new shallow roots not allowed",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
-		print_ref_status('!', "[remote rejected]", ref,
+		print_ref_status('!', "[remote rejected]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 ref->remote_status, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
-		print_ref_status('!', "[remote failure]", ref,
+		print_ref_status('!', "[remote failure]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "atomic push failed", porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v6 6/7] t5412: test proc-receive hook on HTTP protocol
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (20 preceding siblings ...)
  2020-04-02 16:35 ` [PATCH v6 5/7] receive-pack: refactor report for proc-receive Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  2020-04-02 16:35 ` [PATCH v6 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Copy from t5411 to test "proc-receive" hook on HTTP protocol.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5412-proc-receive-hook-http-protocol.sh | 960 +++++++++++++++++++++
 1 file changed, 960 insertions(+)
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
new file mode 100755
index 0000000000..a3a7beca27
--- /dev/null
+++ b/t/t5412-proc-receive-hook-http-protocol.sh
@@ -0,0 +1,960 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook for HTTP protocol'
+
+. ./test-lib.sh
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
+		-e "s#To .*/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d" \
+		-e "/^remote: fatal: /d"
+}
+
+# Asynchronous sideband may generate inconsistent output messages,
+# sort before comparison.
+test_sorted_cmp () {
+	if ! $GIT_TEST_CMP "$@" >/dev/null 2>&1
+	then
+		cmd=$GIT_TEST_CMP
+		for f in "$@"
+		do
+			sort "$f" >"$f.sorted"
+			cmd="$cmd \"$f.sorted\""
+		done
+		if ! eval $cmd >/dev/null 2>&1
+		then
+			$GIT_TEST_CMP "$@"
+		fi
+	fi
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git -C upstream config http.receivepack true &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive &&
+
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for/ &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic:                       next(A)  refs/for/master/topic(A)
+test_expect_failure "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: error: protocol error: unknown proc-receive version "2"
+	remote: error: proc-receive did not exit properly
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_sorted_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success C_LOCALE_OUTPUT "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v6 7/7] doc: add documentation for the proc-receive hook
  2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
                   ` (21 preceding siblings ...)
  2020-04-02 16:35 ` [PATCH v6 6/7] t5412: test proc-receive hook on HTTP protocol Jiang Xin
@ 2020-04-02 16:35 ` Jiang Xin
  22 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-02 16:35 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 70 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..10ea5c1f18 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,76 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  According to refnames of the commands which
+`git push` sends to 'git-receive-pack', the commands will be devided
+into two groups by matching what the `receive.procReceiveRefs`
+configuration variable defines.  One group of the commands will execute
+the internal `execute_commands` function to update the corresponding
+refnames, and the other group of commands which have matching refnames
+will execute this 'proc-receive' hook to create pull requests, etc.
+If there is no `receive.procReceiveRefs` settings, this hook won't
+execute at all, and all commands are sent to the internal
+`execute_commands` function.
+
+Its exit status only determines the success or failure of the group of
+commands with special refnames, unless atomic push is in use.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but will talk a protocol in pkt-line format with the
+'receive-pack' for reading commands, push-options (optional), and
+sending result.  In the following example, The letter "S" stands for
+"receive-pack" and letter "H" stands for the hook.
+
+    # Version and capabilities negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(old-oid new-oid ref)
+    S: ... ...
+    S: flush-pkt
+    # Only if push-options have been negotiated.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(old-oid new-oid ref ok)
+    # NO, I reject it.
+    H: PKT-LINE(old-oid new-oid ref ng reason)
+    # OK, but use an alternate reference.
+    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
+    # It will fallthrough to receive-pack to execute.
+    H: PKT-LINE(old-oid new-oid ref ft)
+    H: ... ...
+    H: flush-pkt
+
+The "proc-receive" hook may update one or more references, and will send
+its result one by one in pkt-line format.  Each line of the result has
+four fields and one optional message field, like "<old-oid> <new-oid>
+<ref> <status> [<message>]".
+
+The first three fields are the same as those of the commands for
+"receive-pack".
+
+The forth field has a two-letter status code.  Available status codes:
+
+* ok: The command runs successfully.  If the optional message has a
+  prefix "ref:", the hook has created/updated an alternate reference
+  instead.
+
+* ng: Fail to run the command.  Error message is given in the optional
+  message field.
+
+* ft: Will fallthrough to receive-pack to execute.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v6 0/7] New proc-receive hook for centralized workflow
  2020-04-02 16:35 ` [PATCH v6 0/7] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-04-02 18:26   ` Junio C Hamano
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
                       ` (7 more replies)
  0 siblings, 8 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-04-02 18:26 UTC (permalink / raw)
  To: Jiang Xin, Jonathan Nieder; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
>
> ## Changes since v5
>
> + Fixed code review issues on v5.
>
>   * s/cannot to/cannot/
>
>   * s/FLEX_ALLOC_MEM/FLEX_ALLOC_STR/

OK

>   * Add flag `C_LOCALE_OUTPUT` on test cases which may break
>     gettext-poison test.

We prefer to avoid the C_LOCALE_OUTPUT prerequiste when we can use
test_i18n<stuff> to make the poison test not care.  If it is too
hard to arrange in these tests, it is OK to omit, though.

>   * Note: `rp_errors()` in "receive-pack" is executed on the
>     server side, I think leave them unmarked for i18n is better.

Hmph, I vaguely recall saying something similar and then got a
disagreement from somebody.  Perhaps Jonathan (cc'ed)?

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v7 0/7] New proc-receive hook for centralized workflow
  2020-04-02 18:26   ` Junio C Hamano
@ 2020-04-03 16:08     ` Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
                         ` (7 more replies)
  2020-04-03 16:08     ` [PATCH v7 1/7] transport: not report a non-head push as a branch Jiang Xin
                       ` (6 subsequent siblings)
  7 siblings, 8 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v6

* Test breaks because of the conflict between `copy_to_sideband()` and
  `rp_error()`.  Store error messages in `errmsg` and output the errmsg
  after sideband muxer closed.

* Use test_i18ncmp instead of mark the whole test case using
  C_LOCALE_OUTPUT.

* Depends on topic `jx/atomic-push`.  One test case will fail without
  it.

## Range-diff v6...v7

1:  ed8d2c0e82 ! 1:  4259d31426 receive-pack: add new proc-receive hook
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +}
     +
     +static int read_proc_receive_result(struct packet_reader *reader,
    -+				    struct command *commands)
    ++				    struct command *commands,
    ++				    struct strbuf *errmsg)
     +{
     +	struct command *hint;
     +	struct command *cmd;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		    *p++ != ' ' ||
     +		    parse_oid_hex(p, &new_oid, &p) ||
     +		    *p++ != ' ') {
    -+			rp_error("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
    -+				 reader->line);
    ++			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
    ++				    reader->line);
     +			return -1;
     +		}
     +
     +		refname = p;
     +		status = strchr(p, ' ');
     +		if (!status) {
    -+			rp_error("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'",
    -+				 reader->line);
    ++			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
    ++				    reader->line);
     +			return -1;
     +		}
     +		*status++ = '\0';
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			*msg++ = '\0';
     +		}
     +		if (strlen(status) != 2) {
    -+			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
    -+				 status, reader->line);
    ++			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    ++				    status, reader->line);
     +			return -1;
     +		}
     +
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		if (!hint)
     +			hint = find_command_by_refname(commands, refname);
     +		if (!hint) {
    -+			warning("proc-receive reported status on unknown ref: %s",
    -+				refname);
    ++			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
    ++				    refname);
     +			continue;
     +		}
     +		if (!hint->run_proc_receive) {
    -+			warning("proc-receive reported status on ref of builtin command: %s",
    -+				refname);
    ++			strbuf_addf(errmsg, "proc-receive reported status on ref of builtin command: %s\n",
    ++				    refname);
     +			continue;
     +		}
     +		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +				hint->error_string = "failed";
     +			code = 1;
     +		} else if (strcmp("ok", status)) {
    -+			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
    -+				 status, reader->line);
    ++			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    ++				    status, reader->line);
     +			return -1;
     +		}
     +	}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	const char *argv[2];
     +	struct packet_reader reader;
     +	struct strbuf cap = STRBUF_INIT;
    ++	struct strbuf errmsg = STRBUF_INIT;
     +	int pr_use_push_options = 0;
     +	int version = 0;
     +	int code;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	}
     +
     +	if (version != 1) {
    -+		rp_error("protocol error: unknown proc-receive version '%d'",
    -+			 version);
    ++		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported\n",
    ++			    version);
     +		code = -1;
     +		goto cleanup;
     +	}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	}
     +
     +	/* Read result from proc-receive */
    -+	code = read_proc_receive_result(&reader, commands);
    ++	code = read_proc_receive_result(&reader, commands, &errmsg);
     +
     +cleanup:
     +	close(proc.in);
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	if (use_sideband)
     +		finish_async(&muxer);
     +	if (finish_command(&proc)) {
    -+		rp_error("proc-receive did not exit properly");
    ++		strbuf_addstr(&errmsg, "proc-receive did not exit properly\n");
     +		code = -1;
     +	}
    ++	if (errmsg.len >0) {
    ++		char *p = errmsg.buf;
    ++
    ++		p += errmsg.len - 1;
    ++		if (*p == '\n')
    ++			*p = '\0';
    ++		rp_error("%s", errmsg.buf);
    ++		strbuf_release(&errmsg);
    ++	}
     +	sigchain_pop(SIGPIPE);
    ++
     +	return code;
     +}
     +
    @@ t/t5411-proc-receive-hook.sh: make_user_friendly_and_stable_output () {
     +		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
     +		-e "s#To ../upstream.git#To <URL/of/upstream.git>#" \
     +		-e "/^error: / d"
    -+}
    -+
    -+# Asynchronous sideband may generate inconsistent output messages,
    -+# sort before comparison.
    -+test_sorted_cmp () {
    -+	if ! $GIT_TEST_CMP "$@" >/dev/null 2>&1
    -+	then
    -+		cmd=$GIT_TEST_CMP
    -+		for f in "$@"
    -+		do
    -+			sort "$f" >"$f.sorted"
    -+			cmd="$cmd \"$f.sorted\""
    -+		done
    -+		if ! eval $cmd >/dev/null 2>&1
    -+		then
    -+			$GIT_TEST_CMP "$@"
    -+		fi
    -+	fi
      }
      
      # Refs of upstream : master(B)  next(A)
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	 * [new branch] HEAD -> next
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push --atomic:                       next(A)  refs/for/master/topic(A)
    -+test_expect_failure "no proc-receive hook, fail all for atomic push" '
    ++test_expect_success "no proc-receive hook, fail all for atomic push" '
     +	test_must_fail git -C workbench push --atomic origin \
     +		HEAD:next \
     +		HEAD:refs/for/master/topic >out 2>&1 &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success C_LOCALE_OUTPUT "proc-receive bad protocol: unknown version" '
    ++test_expect_success "proc-receive bad protocol: unknown version" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
    -+	remote: error: protocol error: unknown proc-receive version "2"
     +	remote: fatal: the remote end hung up unexpectedly
    -+	remote: error: proc-receive did not exit properly
    ++	remote: error: proc-receive version "2" is not supported
    ++	remote: proc-receive did not exit properly
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_i18ncmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> bad-id new-id ref ok
    -+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
    ++	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
    -+	remote: error: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
    -+	remote: error: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
    -+	warning: proc-receive reported status on ref of builtin command: refs/heads/master
    ++	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	To <URL/of/upstream.git>
     +	 <OID>..<OID> <COMMIT-B> -> master
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    -+	warning: proc-receive reported status on unknown ref: refs/for/master/topic
    ++	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -o ...  :                       refs/for/master/topic
    -+test_expect_success C_LOCALE_OUTPUT "not support push options" '
    ++test_expect_success "not support push options" '
     +	test_must_fail git -C workbench push \
     +		-o issue=123 \
     +		-o reviewer=user1 \
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	fatal: the receiving end does not support push options
     +	fatal: the remote end hung up unexpectedly
     +	EOF
    -+	test_cmp expect actual &&
    ++	test_i18ncmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
2:  1aa84fdf6f = 2:  b47379a7fe refs.c: refactor to reuse ref_is_hidden()
3:  a301edb86c = 3:  461f3a4141 receive-pack: new config receive.procReceiveRefs
4:  538fa71f48 ! 4:  82e6d94b0b receive-pack: refactor report for proc-receive
    @@ builtin/receive-pack.c: static int read_proc_receive_result(struct packet_reader
     +			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
     +			hint->run_proc_receive = 0;
     +		} else {
    - 			rp_error("protocol error: proc-receive has bad status '%s' for '%s'",
    - 				 status, reader->line);
    + 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    + 				    status, reader->line);
      			return -1;
     @@ builtin/receive-pack.c: static void report(struct command *commands, const char *unpack_status)
      	packet_buf_write(&buf, "unpack %s\n",
5:  20289547f3 ! 5:  f90bacec1d t5412: test proc-receive hook on HTTP protocol
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +		-e "/^remote: fatal: /d"
     +}
     +
    -+# Asynchronous sideband may generate inconsistent output messages,
    -+# sort before comparison.
    -+test_sorted_cmp () {
    -+	if ! $GIT_TEST_CMP "$@" >/dev/null 2>&1
    -+	then
    -+		cmd=$GIT_TEST_CMP
    -+		for f in "$@"
    -+		do
    -+			sort "$f" >"$f.sorted"
    -+			cmd="$cmd \"$f.sorted\""
    -+		done
    -+		if ! eval $cmd >/dev/null 2>&1
    -+		then
    -+			$GIT_TEST_CMP "$@"
    -+		fi
    -+	fi
    -+}
    -+
     +# Refs of upstream : master(B)  next(A)
     +# Refs of workbench: master(A)           tags/v123
     +test_expect_success "setup" '
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	 * [new branch] HEAD -> next
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push --atomic:                       next(A)  refs/for/master/topic(A)
    -+test_expect_failure "no proc-receive hook, fail all for atomic push" '
    ++test_expect_success "no proc-receive hook, fail all for atomic push" '
     +	test_must_fail git -C workbench push --atomic origin \
     +		HEAD:next \
     +		HEAD:refs/for/master/topic >out 2>&1 &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
    -+	remote: error: protocol error: unknown proc-receive version "2"
    -+	remote: error: proc-receive did not exit properly
    ++	remote: error: proc-receive version "2" is not supported
    ++	remote: proc-receive did not exit properly
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> bad-id new-id ref ok
    -+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
    ++	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: error: protocol error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
    -+	remote: error: protocol error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
    -+	remote: error: protocol error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
    ++	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	To <URL/of/upstream.git>
     +	 <OID>..<OID> <COMMIT-B> -> master
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    ++	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
     +	EOF
    -+	test_sorted_cmp expect actual &&
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -o ...  :                       refs/for/master/topic
    -+test_expect_success C_LOCALE_OUTPUT "not support push options" '
    ++test_expect_success "not support push options" '
     +	test_must_fail git -C workbench push \
     +		-o issue=123 \
     +		-o reviewer=user1 \
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	fatal: the receiving end does not support push options
     +	fatal: the remote end hung up unexpectedly
     +	EOF
    -+	test_cmp expect actual &&
    ++	test_i18ncmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
6:  da9106a42d = 6:  5338d358c3 doc: add documentation for the proc-receive hook

---

Jiang Xin (7):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  receive-pack: refactor report for proc-receive
  t5412: test proc-receive hook on HTTP protocol
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt           |  14 +
 Documentation/githooks.txt                 |  70 ++
 Makefile                                   |   1 +
 builtin/receive-pack.c                     | 312 ++++++-
 refs.c                                     |  11 +-
 refs.h                                     |   1 +
 t/helper/test-proc-receive.c               | 172 ++++
 t/helper/test-tool.c                       |   1 +
 t/helper/test-tool.h                       |   1 +
 t/t5411-proc-receive-hook.sh               | 932 ++++++++++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh | 944 +++++++++++++++++++++
 t/t5516-fetch-push.sh                      |   2 +-
 transport-helper.c                         |  64 +-
 transport.c                                |  62 +-
 14 files changed, 2521 insertions(+), 66 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v7 1/7] transport: not report a non-head push as a branch
  2020-04-02 18:26   ` Junio C Hamano
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
@ 2020-04-03 16:08     ` Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 2/7] receive-pack: add new proc-receive hook Jiang Xin
                       ` (5 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh | 153 +++++++++++++++++++++++++++++++++++
 t/t5516-fetch-push.sh        |   2 +-
 transport.c                  |   9 ++-
 3 files changed, 160 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..1784bcb584
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,153 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To ../upstream
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..272c0f4046 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v7 2/7] receive-pack: add new proc-receive hook
  2020-04-02 18:26   ` Junio C Hamano
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 1/7] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-03 16:08     ` Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                       ` (4 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and capabilities negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt
    # Only if push-options have been negotiated.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(old-oid new-oid ref ok)
    # NO, I reject it.
    H: PKT-LINE(old-oid new-oid ref ng reason)
    # OK, but use an alternate reference. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
    # It will fallthrough to receive-pack to execute. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ft)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook can create/update another alternate
reference.  For example, a command for a reference "refs/for/master" may
create a special reference, such as "refs/pull/123/head".  The alternate
reference can be returned from the result in an extensible format like
"<old-oid> <new-oid> <reference> <status> [<message>]".

The result will be stored in a command list, and "receive-pack" will use
the result to replace the commands that have specific `run_proc_receive`
field turned on.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 264 +++++++++++++++-
 t/helper/test-proc-receive.c | 172 ++++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411-proc-receive-hook.sh | 593 ++++++++++++++++++++++++++++++++++-
 6 files changed, 1018 insertions(+), 14 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..eb583093aa 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +820,234 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for ( ; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_result(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *hint;
+	struct command *cmd;
+	int code = 0;
+
+	hint = NULL;
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ') {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			return -1;
+		}
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status) {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			return -1;
+		}
+		*status++ = '\0';
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
+				    status, reader->line);
+			return -1;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on ref of builtin command: %s\n",
+				    refname);
+			continue;
+		}
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		oidcpy(&hint->old_oid, &old_oid);
+		oidcpy(&hint->new_oid, &new_oid);
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				hint->error_string = xstrdup(msg);
+			else
+				hint->error_string = "failed";
+			code = 1;
+		} else if (strcmp("ok", status)) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
+				    status, reader->line);
+			return -1;
+		}
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED))
+		    cmd->error_string = "no report from proc-receive";
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported\n",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		char *old_hex, *new_hex;
+
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		old_hex = oid_to_hex(&cmd->old_oid);
+		new_hex = oid_to_hex(&cmd->new_oid);
+		packet_write_fmt(proc.in, "%s %s %s",
+				 old_hex, new_hex, cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_result(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc)) {
+		strbuf_addstr(&errmsg, "proc-receive did not exit properly\n");
+		code = -1;
+	}
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1623,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1663,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1699,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1729,20 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1769,18 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive) {
+		int code;
+
+		code = run_proc_receive_hook(commands, push_options);
+		if (code) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+					cmd->error_string = "fail to run proc-receive hook";
+			}
+		}
+	}
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..ed30817905
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,172 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			char *old_hex, *new_hex;
+
+			old_hex = oid_to_hex(&cmd->old_oid);
+			new_hex = oid_to_hex(&cmd->new_oid);
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				old_hex, new_hex, cmd->ref_name);
+		}
+
+		if (push_options.nr > 0) {
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+		}
+
+		if (returns.nr) {
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+		}
+	}
+
+	if (returns.nr) {
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	}
+	packet_flush(1);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 1784bcb584..a62dadb412 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -43,8 +43,11 @@ create_commits_in () {
 # Format the output of git-push, git-show-ref and other commands to make a
 # user-friendly and stable text.  We can easily prepare the expect text
 # without having to worry about future changes of the commit ID and spaces
-# of the output.  We also replce single quotes with double quotes, because
-# it is boring to prepare unquoted single quotes in expect txt.
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
 make_user_friendly_and_stable_output () {
 	sed \
 		-e "s/  *\$//" \
@@ -54,13 +57,16 @@ make_user_friendly_and_stable_output () {
 		-e "s/$B/<COMMIT-B>/g" \
 		-e "s/$TAG/<TAG-v123>/g" \
 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
-		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
+		-e "s#To ../upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
 }
 
 # Refs of upstream : master(B)  next(A)
 # Refs of workbench: master(A)           tags/v123
 test_expect_success "setup" '
-	git init --bare upstream &&
+	upstream=upstream.git &&
+	git init --bare "$upstream" &&
 	git init workbench &&
 	create_commits_in workbench A B &&
 	(
@@ -68,7 +74,7 @@ test_expect_success "setup" '
 		# Try to make a stable fixed width for abbreviated commit ID,
 		# this fixed-width oid will be replaced with "<OID>".
 		git config core.abbrev 7 &&
-		git remote add origin ../upstream &&
+		git remote add origin ../$upstream &&
 		git update-ref refs/heads/master $A &&
 		git tag -m "v123" v123 $A &&
 		git push origin \
@@ -78,7 +84,7 @@ test_expect_success "setup" '
 	TAG=$(git -C workbench rev-parse v123) &&
 
 	# setup pre-receive hook
-	cat >upstream/hooks/pre-receive <<-\EOF &&
+	cat >"$upstream/hooks/pre-receive" <<-\EOF &&
 	#!/bin/sh
 
 	echo >&2 "# pre-receive hook"
@@ -90,7 +96,7 @@ test_expect_success "setup" '
 	EOF
 
 	# setup post-receive hook
-	cat >upstream/hooks/post-receive <<-\EOF &&
+	cat >"$upstream/hooks/post-receive" <<-\EOF &&
 	#!/bin/sh
 
 	echo >&2 "# post-receive hook"
@@ -102,8 +108,8 @@ test_expect_success "setup" '
 	EOF
 
 	chmod a+x \
-		upstream/hooks/pre-receive \
-		upstream/hooks/post-receive
+		"$upstream/hooks/pre-receive" \
+		"$upstream/hooks/post-receive"
 '
 
 # Refs of upstream : master(B)  next(A)
@@ -131,7 +137,7 @@ test_expect_success "normal git-push command" '
 	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
-	To ../upstream
+	To <URL/of/upstream.git>
 	 + <OID>...<OID> HEAD -> master (forced update)
 	 - [deleted] next
 	 * [new tag] v123 -> v123
@@ -139,7 +145,7 @@ test_expect_success "normal git-push command" '
 	 * [new branch] HEAD -> a/b/c
 	EOF
 	test_cmp expect actual &&
-	git -C upstream show-ref >out &&
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	<COMMIT-A> refs/heads/a/b/c
@@ -150,4 +156,569 @@ test_expect_success "normal git-push command" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic:                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: the remote end hung up unexpectedly
+	remote: error: proc-receive version "2" is not supported
+	remote: proc-receive did not exit properly
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_i18ncmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_i18ncmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v7 3/7] refs.c: refactor to reuse ref_is_hidden()
  2020-04-02 18:26   ` Junio C Hamano
                       ` (2 preceding siblings ...)
  2020-04-03 16:08     ` [PATCH v7 2/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-03 16:08     ` Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
                       ` (3 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v7 4/7] receive-pack: new config receive.procReceiveRefs
  2020-04-02 18:26   ` Junio C Hamano
                       ` (3 preceding siblings ...)
  2020-04-03 16:08     ` [PATCH v7 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-03 16:08     ` Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 5/7] receive-pack: refactor report for proc-receive Jiang Xin
                       ` (2 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 ++++
 builtin/receive-pack.c           |  43 ++++++++--
 t/t5411-proc-receive-hook.sh     | 130 +++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..0178f2d478 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for/", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index eb583093aa..8540829b05 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1732,15 +1747,26 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2200,6 +2226,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2315,5 +2343,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index a62dadb412..cc5f1adda4 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -167,6 +167,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for/ &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -721,4 +729,126 @@ test_expect_success "push with options" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v7 5/7] receive-pack: refactor report for proc-receive
  2020-04-02 18:26   ` Junio C Hamano
                       ` (4 preceding siblings ...)
  2020-04-03 16:08     ` [PATCH v7 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-03 16:08     ` Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 6/7] t5412: test proc-receive hook on HTTP protocol Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The "proc-receive" may update one or more references, and will send its
result one by one in pkt-line format.  Each line of the result has four
fields and one optional message field, as "<old-oid> <new-oid> <ref>
<status> [<message>]".  See the following example:

    # OK, run this command successfully.
    PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference.
    PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute.
    PKT-LINE(old-oid new-oid ref ft)

The first three fields have the same foramt as a command.

The forth field has a two-letter status code.  Available status code:

* ok: The command runs successfully.  If the optional message has a
  prefix "ref:", the hook has created/updated an alternate reference
  instead.

* ng: Fail to run the command. Error message is in the optional message
  field.

* ft: Will fallthrough to receive-pack to execute.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 21 +++++++--
 t/t5411-proc-receive-hook.sh | 90 +++++++++++++++++++++++++++++++++---
 transport-helper.c           | 64 ++++++++++++-------------
 transport.c                  | 59 +++++++++++++----------
 4 files changed, 168 insertions(+), 66 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 8540829b05..47138e8fa8 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -328,6 +328,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extra_string;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     run_proc_receive:2;
@@ -913,7 +914,12 @@ static int read_proc_receive_result(struct packet_reader *reader,
 			else
 				hint->error_string = "failed";
 			code = 1;
-		} else if (strcmp("ok", status)) {
+		} else if (!strcmp("ok", status)) {
+			hint->extra_string = xstrdup_or_null(msg);
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else {
 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
 				    status, reader->line);
 			return -1;
@@ -2183,12 +2189,17 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
+		if (!cmd->error_string) {
+			if (!cmd->extra_string)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			else
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, ' ', cmd->extra_string);
+		} else {
 			packet_buf_write(&buf, "ng %s %s\n",
 					 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index cc5f1adda4..07a3742068 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -743,8 +743,8 @@ test_expect_success "setup proc-receive hook" '
 
 	test-tool proc-receive -v \
 		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
-		-r "$ZERO_OID $A refs/for/next/topic ok" \
-		-r "$ZERO_OID $A refs/for/master/topic ok"
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
 	EOF
 	chmod a+x "$upstream/hooks/proc-receive"
 '
@@ -769,16 +769,16 @@ test_expect_success "report update of all special refs" '
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/review/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -807,6 +807,7 @@ test_expect_success "setup proc-receive hook" '
 # git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
 test_expect_success "report mixed refs update" '
 	git -C workbench push origin \
+		$B:refs/heads/master \
 		HEAD:refs/heads/bar \
 		HEAD:refs/heads/baz \
 		HEAD:refs/for/next/topic \
@@ -816,6 +817,7 @@ test_expect_success "report mixed refs update" '
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
@@ -827,12 +829,14 @@ test_expect_success "report mixed refs update" '
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
 	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
@@ -846,6 +850,80 @@ test_expect_success "report mixed refs update" '
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/master
 	EOF
 	test_cmp expect actual
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..cec3495d59 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
diff --git a/transport.c b/transport.c
index 272c0f4046..28731fa014 100644
--- a/transport.c
+++ b/transport.c
@@ -459,15 +459,18 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 	}
 }
 
-static void print_ref_status(char flag, const char *summary,
+static void print_ref_status(char flag, const char *summary, char *target_refname,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	if (!target_refname)
+		target_refname = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, target_refname);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, target_refname);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +484,9 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(target_refname));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(target_refname), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -495,18 +498,26 @@ static void print_ref_status(char flag, const char *summary,
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
 {
+	char *refname;
+
+	if (ref->remote_status && !strncmp(ref->remote_status, "ref:", 4))
+		refname = ref->remote_status + 4;
+	else
+		refname = ref->name;
+
 	if (ref->deletion)
-		print_ref_status('-', "[deleted]", ref, NULL, NULL,
+		print_ref_status('-', "[deleted]", refname, ref, NULL, NULL,
 				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+	else if (is_null_oid(&ref->old_oid)) {
+
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(refname, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(refname, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
-	else {
+				 refname, ref, ref->peer_ref, NULL, porcelain, summary_width);
+	} else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
@@ -525,7 +536,7 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
 					 DEFAULT_ABBREV);
 
-		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
+		print_ref_status(type, quickref.buf, refname, ref, ref->peer_ref, msg,
 				 porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
@@ -542,56 +553,56 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 
 	switch(ref->status) {
 	case REF_STATUS_NONE:
-		print_ref_status('X', "[no match]", ref, NULL, NULL,
+		print_ref_status('X', "[no match]", NULL, ref, NULL, NULL,
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
-		print_ref_status('!', "[rejected]", ref, NULL,
+		print_ref_status('!', "[rejected]", NULL, ref, NULL,
 				 "remote does not support deleting refs",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
-		print_ref_status('=', "[up to date]", ref,
+		print_ref_status('=', "[up to date]", NULL, ref,
 				 ref->peer_ref, NULL, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "non-fast-forward", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "already exists", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "fetch first", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "needs force", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "stale info", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "new shallow roots not allowed",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
-		print_ref_status('!', "[remote rejected]", ref,
+		print_ref_status('!', "[remote rejected]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 ref->remote_status, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
-		print_ref_status('!', "[remote failure]", ref,
+		print_ref_status('!', "[remote failure]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "atomic push failed", porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v7 6/7] t5412: test proc-receive hook on HTTP protocol
  2020-04-02 18:26   ` Junio C Hamano
                       ` (5 preceding siblings ...)
  2020-04-03 16:08     ` [PATCH v7 5/7] receive-pack: refactor report for proc-receive Jiang Xin
@ 2020-04-03 16:08     ` Jiang Xin
  2020-04-03 16:08     ` [PATCH v7 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Copy from t5411 to test "proc-receive" hook on HTTP protocol.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5412-proc-receive-hook-http-protocol.sh | 944 +++++++++++++++++++++
 1 file changed, 944 insertions(+)
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
new file mode 100755
index 0000000000..fc3dca9d78
--- /dev/null
+++ b/t/t5412-proc-receive-hook-http-protocol.sh
@@ -0,0 +1,944 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook for HTTP protocol'
+
+. ./test-lib.sh
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
+		-e "s#To .*/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d" \
+		-e "/^remote: fatal: /d"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git -C upstream config http.receivepack true &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive &&
+
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for/ &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic:                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: error: proc-receive version "2" is not supported
+	remote: proc-receive did not exit properly
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_i18ncmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v7 7/7] doc: add documentation for the proc-receive hook
  2020-04-02 18:26   ` Junio C Hamano
                       ` (6 preceding siblings ...)
  2020-04-03 16:08     ` [PATCH v7 6/7] t5412: test proc-receive hook on HTTP protocol Jiang Xin
@ 2020-04-03 16:08     ` Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-03 16:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 70 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..10ea5c1f18 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,76 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  According to refnames of the commands which
+`git push` sends to 'git-receive-pack', the commands will be devided
+into two groups by matching what the `receive.procReceiveRefs`
+configuration variable defines.  One group of the commands will execute
+the internal `execute_commands` function to update the corresponding
+refnames, and the other group of commands which have matching refnames
+will execute this 'proc-receive' hook to create pull requests, etc.
+If there is no `receive.procReceiveRefs` settings, this hook won't
+execute at all, and all commands are sent to the internal
+`execute_commands` function.
+
+Its exit status only determines the success or failure of the group of
+commands with special refnames, unless atomic push is in use.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but will talk a protocol in pkt-line format with the
+'receive-pack' for reading commands, push-options (optional), and
+sending result.  In the following example, The letter "S" stands for
+"receive-pack" and letter "H" stands for the hook.
+
+    # Version and capabilities negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(old-oid new-oid ref)
+    S: ... ...
+    S: flush-pkt
+    # Only if push-options have been negotiated.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(old-oid new-oid ref ok)
+    # NO, I reject it.
+    H: PKT-LINE(old-oid new-oid ref ng reason)
+    # OK, but use an alternate reference.
+    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
+    # It will fallthrough to receive-pack to execute.
+    H: PKT-LINE(old-oid new-oid ref ft)
+    H: ... ...
+    H: flush-pkt
+
+The "proc-receive" hook may update one or more references, and will send
+its result one by one in pkt-line format.  Each line of the result has
+four fields and one optional message field, like "<old-oid> <new-oid>
+<ref> <status> [<message>]".
+
+The first three fields are the same as those of the commands for
+"receive-pack".
+
+The forth field has a two-letter status code.  Available status codes:
+
+* ok: The command runs successfully.  If the optional message has a
+  prefix "ref:", the hook has created/updated an alternate reference
+  instead.
+
+* ng: Fail to run the command.  Error message is given in the optional
+  message field.
+
+* ft: Will fallthrough to receive-pack to execute.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v8 0/7] New proc-receive hook for centralized workflow
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
                           ` (6 more replies)
  2020-04-04 13:43       ` [PATCH v8 1/7] transport: not report a non-head push as a branch Jiang Xin
                         ` (6 subsequent siblings)
  7 siblings, 7 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>


## Changes since v7

* Parallel test (make -j7 test) failed because of the failure message on
  calling `finish_command()` is not stable for test.  Remove the error
  message in this revision.

    -	if (finish_command(&proc)) {
    -		strbuf_addstr(&errmsg, "proc-receive did not exit properly\n");
    +	if (finish_command(&proc))
     		code = -1;
    -	}


## Changes since v6

* Test failed because two message streams of `copy_to_sideband()` and
  `rp_error()` are mixed together.  Store error messages in `errmsg`
  and output the errmsg after closing the sideband muxer.

* Use test_i18ncmp instead of mark the whole test case using
  C_LOCALE_OUTPUT.

* Please merge this topic after topic `jx/atomic-push`.  One test case
  depends on it.


## Range-diff v6...v7

1:  64c9ca7086 = 1:  19c66785d1 transport: not report a non-head push as a branch
2:  0d27cc5e80 ! 2:  085ded61f5 receive-pack: add new proc-receive hook
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		    *p++ != ' ' ||
     +		    parse_oid_hex(p, &new_oid, &p) ||
     +		    *p++ != ' ') {
    -+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
    ++			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'",
     +				    reader->line);
     +			return -1;
     +		}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		refname = p;
     +		status = strchr(p, ' ');
     +		if (!status) {
    -+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
    ++			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'",
     +				    reader->line);
     +			return -1;
     +		}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			*msg++ = '\0';
     +		}
     +		if (strlen(status) != 2) {
    -+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    ++			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
     +				    status, reader->line);
     +			return -1;
     +		}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +				hint->error_string = xstrdup(msg);
     +			else
     +				hint->error_string = "failed";
    -+			code = 1;
    ++			code = -1;
     +		} else if (strcmp("ok", status)) {
    -+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    ++			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
     +				    status, reader->line);
     +			return -1;
     +		}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +	for (cmd = commands; cmd; cmd = cmd->next)
     +		if (cmd->run_proc_receive &&
    -+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED))
    ++		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
     +		    cmd->error_string = "no report from proc-receive";
    ++		    code = -1;
    ++		}
     +
     +	return code;
     +}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	}
     +
     +	if (version != 1) {
    -+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported\n",
    ++		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
     +			    version);
     +		code = -1;
     +		goto cleanup;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	close(proc.out);
     +	if (use_sideband)
     +		finish_async(&muxer);
    -+	if (finish_command(&proc)) {
    -+		strbuf_addstr(&errmsg, "proc-receive did not exit properly\n");
    ++	if (finish_command(&proc))
     +		code = -1;
    -+	}
     +	if (errmsg.len >0) {
     +		char *p = errmsg.buf;
     +
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: fatal: the remote end hung up unexpectedly
     +	remote: error: proc-receive version "2" is not supported
    -+	remote: proc-receive did not exit properly
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
3:  81f086188c = 3:  230f28198f refs.c: refactor to reuse ref_is_hidden()
4:  52675b55a2 ! 4:  e6a7608a84 receive-pack: new config receive.procReceiveRefs
    @@ t/t5411-proc-receive-hook.sh: test_expect_success "cleanup" '
     +test_expect_success "add two receive.procReceiveRefs settings" '
     +	(
     +		cd "$upstream" &&
    -+		git config --add receive.procReceiveRefs refs/for/ &&
    ++		git config --add receive.procReceiveRefs refs/for &&
     +		git config --add receive.procReceiveRefs refs/review/
     +	)
     +'
5:  65876b1e75 ! 5:  e426775925 receive-pack: refactor report for proc-receive
    @@ builtin/receive-pack.c: static void write_head_info(void)
     @@ builtin/receive-pack.c: static int read_proc_receive_result(struct packet_reader *reader,
      			else
      				hint->error_string = "failed";
    - 			code = 1;
    + 			code = -1;
     -		} else if (strcmp("ok", status)) {
     +		} else if (!strcmp("ok", status)) {
     +			hint->extra_string = xstrdup_or_null(msg);
    @@ builtin/receive-pack.c: static int read_proc_receive_result(struct packet_reader
     +			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
     +			hint->run_proc_receive = 0;
     +		} else {
    - 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    + 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
      				    status, reader->line);
      			return -1;
     @@ builtin/receive-pack.c: static void report(struct command *commands, const char *unpack_status)
6:  04ec372c35 ! 6:  c5982067be t5412: test proc-receive hook on HTTP protocol
    @@ Metadata
     Author: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## Commit message ##
    -    t5412: test proc-receive hook on HTTP protocol
    +    t5412: test the proc-receive hook on HTTP protocol
     
    -    Copy from t5411 to test "proc-receive" hook on HTTP protocol.
    +    Copy from t5411 to test the "proc-receive" hook on the HTTP protocol.
     
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +		-e "s/$ZERO_OID/<ZERO-OID>/g" \
     +		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
     +		-e "s#To .*/upstream.git#To <URL/of/upstream.git>#" \
    -+		-e "/^error: / d" \
    -+		-e "/^remote: fatal: /d"
    ++		-e "/^error: / d"
     +}
     +
     +# Refs of upstream : master(B)  next(A)
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +test_expect_success "add two receive.procReceiveRefs settings" '
     +	(
     +		cd "$upstream" &&
    -+		git config --add receive.procReceiveRefs refs/for/ &&
    ++		git config --add receive.procReceiveRefs refs/for &&
     +		git config --add receive.procReceiveRefs refs/review/
     +	)
     +'
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    ++	# Apache calls "receive-pack" with system locale settings, which
    ++	# makes the message "remote: fatal: ..." unstable for test.
    ++	sed -e "/^remote: fatal: / d" out | \
    ++	make_user_friendly_and_stable_output >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: error: proc-receive version "2" is not supported
    -+	remote: proc-receive did not exit properly
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
     +	EOF
7:  4ab8873f12 = 7:  823b7f2ea6 doc: add documentation for the proc-receive hook

---

Jiang Xin (7):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  receive-pack: refactor report for proc-receive
  t5412: test the proc-receive hook on HTTP protocol
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt           |  14 +
 Documentation/githooks.txt                 |  70 ++
 Makefile                                   |   1 +
 builtin/receive-pack.c                     | 312 ++++++-
 refs.c                                     |  11 +-
 refs.h                                     |   1 +
 t/helper/test-proc-receive.c               | 172 ++++
 t/helper/test-tool.c                       |   1 +
 t/helper/test-tool.h                       |   1 +
 t/t5411-proc-receive-hook.sh               | 931 ++++++++++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh | 945 +++++++++++++++++++++
 t/t5516-fetch-push.sh                      |   2 +-
 transport-helper.c                         |  64 +-
 transport.c                                |  62 +-
 14 files changed, 2521 insertions(+), 66 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v8 1/7] transport: not report a non-head push as a branch
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 2/7] receive-pack: add new proc-receive hook Jiang Xin
                         ` (5 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh | 153 +++++++++++++++++++++++++++++++++++
 t/t5516-fetch-push.sh        |   2 +-
 transport.c                  |   9 ++-
 3 files changed, 160 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..1784bcb584
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,153 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To ../upstream
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C upstream show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 75c5c9fe98..4f91c8ea17 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v8 2/7] receive-pack: add new proc-receive hook
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 1/7] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                         ` (4 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and capabilities negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt
    # Only if push-options have been negotiated.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(old-oid new-oid ref ok)
    # NO, I reject it.
    H: PKT-LINE(old-oid new-oid ref ng reason)
    # OK, but use an alternate reference. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
    # It will fallthrough to receive-pack to execute. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ft)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook can create/update another alternate
reference.  For example, a command for a reference "refs/for/master" may
create a special reference, such as "refs/pull/123/head".  The alternate
reference can be returned from the result in an extensible format like
"<old-oid> <new-oid> <reference> <status> [<message>]".

The result will be stored in a command list, and "receive-pack" will use
the result to replace the commands that have specific `run_proc_receive`
field turned on.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 264 +++++++++++++++-
 t/helper/test-proc-receive.c | 172 ++++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411-proc-receive-hook.sh | 592 ++++++++++++++++++++++++++++++++++-
 6 files changed, 1017 insertions(+), 14 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..0760536d58 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +820,234 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for ( ; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_result(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *hint;
+	struct command *cmd;
+	int code = 0;
+
+	hint = NULL;
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ') {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'",
+				    reader->line);
+			return -1;
+		}
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status) {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'",
+				    reader->line);
+			return -1;
+		}
+		*status++ = '\0';
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
+				    status, reader->line);
+			return -1;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on ref of builtin command: %s\n",
+				    refname);
+			continue;
+		}
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		oidcpy(&hint->old_oid, &old_oid);
+		oidcpy(&hint->new_oid, &new_oid);
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				hint->error_string = xstrdup(msg);
+			else
+				hint->error_string = "failed";
+			code = -1;
+		} else if (strcmp("ok", status)) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
+				    status, reader->line);
+			return -1;
+		}
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "no report from proc-receive";
+		    code = -1;
+		}
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		char *old_hex, *new_hex;
+
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		old_hex = oid_to_hex(&cmd->old_oid);
+		new_hex = oid_to_hex(&cmd->new_oid);
+		packet_write_fmt(proc.in, "%s %s %s",
+				 old_hex, new_hex, cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_result(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1623,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1663,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1699,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1729,20 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1769,18 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive) {
+		int code;
+
+		code = run_proc_receive_hook(commands, push_options);
+		if (code) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+					cmd->error_string = "fail to run proc-receive hook";
+			}
+		}
+	}
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..ed30817905
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,172 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			char *old_hex, *new_hex;
+
+			old_hex = oid_to_hex(&cmd->old_oid);
+			new_hex = oid_to_hex(&cmd->new_oid);
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				old_hex, new_hex, cmd->ref_name);
+		}
+
+		if (push_options.nr > 0) {
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+		}
+
+		if (returns.nr) {
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+		}
+	}
+
+	if (returns.nr) {
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	}
+	packet_flush(1);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 1784bcb584..dbcf9f165c 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -43,8 +43,11 @@ create_commits_in () {
 # Format the output of git-push, git-show-ref and other commands to make a
 # user-friendly and stable text.  We can easily prepare the expect text
 # without having to worry about future changes of the commit ID and spaces
-# of the output.  We also replce single quotes with double quotes, because
-# it is boring to prepare unquoted single quotes in expect txt.
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
 make_user_friendly_and_stable_output () {
 	sed \
 		-e "s/  *\$//" \
@@ -54,13 +57,16 @@ make_user_friendly_and_stable_output () {
 		-e "s/$B/<COMMIT-B>/g" \
 		-e "s/$TAG/<TAG-v123>/g" \
 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
-		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
+		-e "s#To ../upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
 }
 
 # Refs of upstream : master(B)  next(A)
 # Refs of workbench: master(A)           tags/v123
 test_expect_success "setup" '
-	git init --bare upstream &&
+	upstream=upstream.git &&
+	git init --bare "$upstream" &&
 	git init workbench &&
 	create_commits_in workbench A B &&
 	(
@@ -68,7 +74,7 @@ test_expect_success "setup" '
 		# Try to make a stable fixed width for abbreviated commit ID,
 		# this fixed-width oid will be replaced with "<OID>".
 		git config core.abbrev 7 &&
-		git remote add origin ../upstream &&
+		git remote add origin ../$upstream &&
 		git update-ref refs/heads/master $A &&
 		git tag -m "v123" v123 $A &&
 		git push origin \
@@ -78,7 +84,7 @@ test_expect_success "setup" '
 	TAG=$(git -C workbench rev-parse v123) &&
 
 	# setup pre-receive hook
-	cat >upstream/hooks/pre-receive <<-\EOF &&
+	cat >"$upstream/hooks/pre-receive" <<-\EOF &&
 	#!/bin/sh
 
 	echo >&2 "# pre-receive hook"
@@ -90,7 +96,7 @@ test_expect_success "setup" '
 	EOF
 
 	# setup post-receive hook
-	cat >upstream/hooks/post-receive <<-\EOF &&
+	cat >"$upstream/hooks/post-receive" <<-\EOF &&
 	#!/bin/sh
 
 	echo >&2 "# post-receive hook"
@@ -102,8 +108,8 @@ test_expect_success "setup" '
 	EOF
 
 	chmod a+x \
-		upstream/hooks/pre-receive \
-		upstream/hooks/post-receive
+		"$upstream/hooks/pre-receive" \
+		"$upstream/hooks/post-receive"
 '
 
 # Refs of upstream : master(B)  next(A)
@@ -131,7 +137,7 @@ test_expect_success "normal git-push command" '
 	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
-	To ../upstream
+	To <URL/of/upstream.git>
 	 + <OID>...<OID> HEAD -> master (forced update)
 	 - [deleted] next
 	 * [new tag] v123 -> v123
@@ -139,7 +145,7 @@ test_expect_success "normal git-push command" '
 	 * [new branch] HEAD -> a/b/c
 	EOF
 	test_cmp expect actual &&
-	git -C upstream show-ref >out &&
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	<COMMIT-A> refs/heads/a/b/c
@@ -150,4 +156,568 @@ test_expect_success "normal git-push command" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic:                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: the remote end hung up unexpectedly
+	remote: error: proc-receive version "2" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_i18ncmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_i18ncmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v8 3/7] refs.c: refactor to reuse ref_is_hidden()
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
                         ` (2 preceding siblings ...)
  2020-04-04 13:43       ` [PATCH v8 2/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
                         ` (3 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v8 4/7] receive-pack: new config receive.procReceiveRefs
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
                         ` (3 preceding siblings ...)
  2020-04-04 13:43       ` [PATCH v8 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 5/7] receive-pack: refactor report for proc-receive Jiang Xin
                         ` (2 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 ++++
 builtin/receive-pack.c           |  43 ++++++++--
 t/t5411-proc-receive-hook.sh     | 130 +++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..0178f2d478 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for/", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 0760536d58..26e561d5b8 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1732,15 +1747,26 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2200,6 +2226,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2315,5 +2343,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index dbcf9f165c..b04a7ec814 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -167,6 +167,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -720,4 +728,126 @@ test_expect_success "push with options" '
 	test_cmp expect actual
 '
 
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v8 5/7] receive-pack: refactor report for proc-receive
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
                         ` (4 preceding siblings ...)
  2020-04-04 13:43       ` [PATCH v8 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 6/7] t5412: test the proc-receive hook on HTTP protocol Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The "proc-receive" may update one or more references, and will send its
result one by one in pkt-line format.  Each line of the result has four
fields and one optional message field, as "<old-oid> <new-oid> <ref>
<status> [<message>]".  See the following example:

    # OK, run this command successfully.
    PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference.
    PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute.
    PKT-LINE(old-oid new-oid ref ft)

The first three fields have the same foramt as a command.

The forth field has a two-letter status code.  Available status code:

* ok: The command runs successfully.  If the optional message has a
  prefix "ref:", the hook has created/updated an alternate reference
  instead.

* ng: Fail to run the command. Error message is in the optional message
  field.

* ft: Will fallthrough to receive-pack to execute.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 21 +++++++--
 t/t5411-proc-receive-hook.sh | 90 +++++++++++++++++++++++++++++++++---
 transport-helper.c           | 64 ++++++++++++-------------
 transport.c                  | 59 +++++++++++++----------
 4 files changed, 168 insertions(+), 66 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 26e561d5b8..3843ae0fe9 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -328,6 +328,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extra_string;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     run_proc_receive:2;
@@ -913,7 +914,12 @@ static int read_proc_receive_result(struct packet_reader *reader,
 			else
 				hint->error_string = "failed";
 			code = -1;
-		} else if (strcmp("ok", status)) {
+		} else if (!strcmp("ok", status)) {
+			hint->extra_string = xstrdup_or_null(msg);
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else {
 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
 				    status, reader->line);
 			return -1;
@@ -2183,12 +2189,17 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
+		if (!cmd->error_string) {
+			if (!cmd->extra_string)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			else
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, ' ', cmd->extra_string);
+		} else {
 			packet_buf_write(&buf, "ng %s %s\n",
 					 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index b04a7ec814..6104641759 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -742,8 +742,8 @@ test_expect_success "setup proc-receive hook" '
 
 	test-tool proc-receive -v \
 		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
-		-r "$ZERO_OID $A refs/for/next/topic ok" \
-		-r "$ZERO_OID $A refs/for/master/topic ok"
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
 	EOF
 	chmod a+x "$upstream/hooks/proc-receive"
 '
@@ -768,16 +768,16 @@ test_expect_success "report update of all special refs" '
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/review/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -806,6 +806,7 @@ test_expect_success "setup proc-receive hook" '
 # git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
 test_expect_success "report mixed refs update" '
 	git -C workbench push origin \
+		$B:refs/heads/master \
 		HEAD:refs/heads/bar \
 		HEAD:refs/heads/baz \
 		HEAD:refs/for/next/topic \
@@ -815,6 +816,7 @@ test_expect_success "report mixed refs update" '
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
@@ -826,12 +828,14 @@ test_expect_success "report mixed refs update" '
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
 	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
@@ -845,6 +849,80 @@ test_expect_success "report mixed refs update" '
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/master
 	EOF
 	test_cmp expect actual
diff --git a/transport-helper.c b/transport-helper.c
index a46afcb69d..0c835c5495 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
diff --git a/transport.c b/transport.c
index 4f91c8ea17..ee9b2a0d8e 100644
--- a/transport.c
+++ b/transport.c
@@ -459,15 +459,18 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 	}
 }
 
-static void print_ref_status(char flag, const char *summary,
+static void print_ref_status(char flag, const char *summary, char *target_refname,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	if (!target_refname)
+		target_refname = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, target_refname);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, target_refname);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +484,9 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(target_refname));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(target_refname), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -495,18 +498,26 @@ static void print_ref_status(char flag, const char *summary,
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
 {
+	char *refname;
+
+	if (ref->remote_status && !strncmp(ref->remote_status, "ref:", 4))
+		refname = ref->remote_status + 4;
+	else
+		refname = ref->name;
+
 	if (ref->deletion)
-		print_ref_status('-', "[deleted]", ref, NULL, NULL,
+		print_ref_status('-', "[deleted]", refname, ref, NULL, NULL,
 				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+	else if (is_null_oid(&ref->old_oid)) {
+
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(refname, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(refname, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
-	else {
+				 refname, ref, ref->peer_ref, NULL, porcelain, summary_width);
+	} else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
@@ -525,7 +536,7 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
 					 DEFAULT_ABBREV);
 
-		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
+		print_ref_status(type, quickref.buf, refname, ref, ref->peer_ref, msg,
 				 porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
@@ -542,56 +553,56 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 
 	switch(ref->status) {
 	case REF_STATUS_NONE:
-		print_ref_status('X', "[no match]", ref, NULL, NULL,
+		print_ref_status('X', "[no match]", NULL, ref, NULL, NULL,
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
-		print_ref_status('!', "[rejected]", ref, NULL,
+		print_ref_status('!', "[rejected]", NULL, ref, NULL,
 				 "remote does not support deleting refs",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
-		print_ref_status('=', "[up to date]", ref,
+		print_ref_status('=', "[up to date]", NULL, ref,
 				 ref->peer_ref, NULL, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "non-fast-forward", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "already exists", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "fetch first", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "needs force", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "stale info", porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "new shallow roots not allowed",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
-		print_ref_status('!', "[remote rejected]", ref,
+		print_ref_status('!', "[remote rejected]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 ref->remote_status, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
-		print_ref_status('!', "[remote failure]", ref,
+		print_ref_status('!', "[remote failure]", NULL, ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
 				 porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
-		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
 				 "atomic push failed", porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v8 6/7] t5412: test the proc-receive hook on HTTP protocol
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
                         ` (5 preceding siblings ...)
  2020-04-04 13:43       ` [PATCH v8 5/7] receive-pack: refactor report for proc-receive Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  2020-04-04 13:43       ` [PATCH v8 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Copy from t5411 to test the "proc-receive" hook on the HTTP protocol.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5412-proc-receive-hook-http-protocol.sh | 945 +++++++++++++++++++++
 1 file changed, 945 insertions(+)
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
new file mode 100755
index 0000000000..b16056e024
--- /dev/null
+++ b/t/t5412-proc-receive-hook-http-protocol.sh
@@ -0,0 +1,945 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook for HTTP protocol'
+
+. ./test-lib.sh
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{})
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
+		-e "s#To .*/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream &&
+	git -C upstream config http.receivepack true &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream/hooks/pre-receive \
+		upstream/hooks/post-receive &&
+
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic:                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	# Apache calls "receive-pack" with system locale settings, which
+	# makes the message "remote: fatal: ..." unstable for test.
+	sed -e "/^remote: fatal: / d" out | \
+	make_user_friendly_and_stable_output >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: error: proc-receive version "2" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	fatal: the receiving end does not support push options
+	fatal: the remote end hung up unexpectedly
+	EOF
+	test_i18ncmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v8 7/7] doc: add documentation for the proc-receive hook
  2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
                         ` (6 preceding siblings ...)
  2020-04-04 13:43       ` [PATCH v8 6/7] t5412: test the proc-receive hook on HTTP protocol Jiang Xin
@ 2020-04-04 13:43       ` Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-04 13:43 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 70 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..10ea5c1f18 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,76 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  According to refnames of the commands which
+`git push` sends to 'git-receive-pack', the commands will be devided
+into two groups by matching what the `receive.procReceiveRefs`
+configuration variable defines.  One group of the commands will execute
+the internal `execute_commands` function to update the corresponding
+refnames, and the other group of commands which have matching refnames
+will execute this 'proc-receive' hook to create pull requests, etc.
+If there is no `receive.procReceiveRefs` settings, this hook won't
+execute at all, and all commands are sent to the internal
+`execute_commands` function.
+
+Its exit status only determines the success or failure of the group of
+commands with special refnames, unless atomic push is in use.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but will talk a protocol in pkt-line format with the
+'receive-pack' for reading commands, push-options (optional), and
+sending result.  In the following example, The letter "S" stands for
+"receive-pack" and letter "H" stands for the hook.
+
+    # Version and capabilities negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(old-oid new-oid ref)
+    S: ... ...
+    S: flush-pkt
+    # Only if push-options have been negotiated.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(old-oid new-oid ref ok)
+    # NO, I reject it.
+    H: PKT-LINE(old-oid new-oid ref ng reason)
+    # OK, but use an alternate reference.
+    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
+    # It will fallthrough to receive-pack to execute.
+    H: PKT-LINE(old-oid new-oid ref ft)
+    H: ... ...
+    H: flush-pkt
+
+The "proc-receive" hook may update one or more references, and will send
+its result one by one in pkt-line format.  Each line of the result has
+four fields and one optional message field, like "<old-oid> <new-oid>
+<ref> <status> [<message>]".
+
+The first three fields are the same as those of the commands for
+"receive-pack".
+
+The forth field has a two-letter status code.  Available status codes:
+
+* ok: The command runs successfully.  If the optional message has a
+  prefix "ref:", the hook has created/updated an alternate reference
+  instead.
+
+* ng: Fail to run the command.  Error message is given in the optional
+  message field.
+
+* ft: Will fallthrough to receive-pack to execute.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v9 0/6] New proc-receive hook for centralized workflow
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
@ 2020-04-07 12:08         ` Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
                             ` (8 more replies)
  2020-04-07 12:08         ` [PATCH v9 1/6] transport: not report a non-head push as a branch Jiang Xin
                           ` (5 subsequent siblings)
  6 siblings, 9 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-07 12:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v8

* One test case failed in CI for `pu` branch (see:
  https://github.com/git/git/runs/565992235) with the following error:

        --- expect	2020-04-06 23:41:27.552286900 +0000
        +++ actual	2020-04-06 23:41:27.458511100 +0000
        @@ -3,6 +3,7 @@
         remote: # proc-receive hook
         remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
         remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
        +remote: fatal: unable to write flush packet: Broken pipe
         remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
         To <URL/of/upstream.git>
          ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
        error: last command exited with $?=1
        not ok 16 - proc-receive bad protocol: no status

  This can be reproduced with the following changes on "t/helper/test-proc-receive.c":

        +++ b/t/helper/test-proc-receive.c
        @@ -2,6 +2,7 @@
         #include "connect.h"
         #include "parse-options.h"
         #include "pkt-line.h"
        +#include "sigchain.h"
         #include "string-list.h"
         #include "test-tool.h"
         
        @@ -130,6 +131,7 @@ int cmd__proc_receive(int argc, const char **argv)
                                   PACKET_READ_CHOMP_NEWLINE |
                                   PACKET_READ_DIE_ON_ERR_PACKET);
         
        +       sigchain_push(SIGPIPE, SIG_IGN);
                proc_receive_verison(&reader);
                proc_receive_read_commands(&reader, &commands);
                proc_receive_read_push_options(&reader, &push_options);
        @@ -163,10 +165,14 @@ int cmd__proc_receive(int argc, const char **argv)
                }
         
                if (returns.nr) {
        -               for_each_string_list_item(item, &returns)
        +               for_each_string_list_item(item, &returns) {
                                packet_write_fmt(1, "%s\n", item->string);
        +                       sleep(1);
        +               }
                }
                packet_flush(1);
        +       sigchain_pop(SIGPIPE);
         
                return 0;
         }

  "test-proc-receive" send results one by one to "receive-pack" and send a 
  flush-pkt to end the communication.  But "receive-pack" will close the pipe,
  if a syntax error is found in the result sent from "test-proc-receive".
  The closed pipe makes "test-proc-receive" complain.

  Make modifications in patch 2/6 of this reroll, so we don't have to adjust
  test cases. E.g.,
  
         /* receive-pack */
         static int read_proc_receive_result(...)
         {
             ... ...
             for (;;) {
                 ... ...
         
                 if (packet_reader_read(reader) != PACKET_READ_NORMAL)
                         break;
                 if (parse_oid_hex(reader->line, &old_oid, &p) ||
                     *p++ != ' ' ||
                     parse_oid_hex(p, &new_oid, &p) ||
                     *p++ != ' ') {
                         strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
                                     reader->line);
        -                return -1;
        +                code = -1;
        +                continue;
                 }


* Patch 1/6: Add `t/t5411/common-functions.sh` and `t/t5411/common-test-cases.sh` to
  reuse test cases between t5411 and t5412.

* Refactor for a smaller patch 5/6 (receive-pack: refactor report for proc-receive).


## Range-diff v8...v9

1:  19c66785d1 ! 1:  ba6222899b transport: not report a non-head push as a branch

    t/t5411-proc-receive-hook.sh               | 75 ++++++++++++++++++++++++++
    t/t5411/common-functions.sh                | 49 +++++++++++++++++
    t/t5411/common-test-cases.sh               | 43 +++++++++++++++
    t/t5412-proc-receive-hook-http-protocol.sh | 86 ++++++++++++++++++++++++++++++

2:  085ded61f5 ! 2:  195c5b0a0c receive-pack: add new proc-receive hook
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		    *p++ != ' ' ||
     +		    parse_oid_hex(p, &new_oid, &p) ||
     +		    *p++ != ' ') {
    -+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'",
    ++			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
     +				    reader->line);
    -+			return -1;
    ++			code = -1;
    ++			continue;
     +		}
     +
     +		refname = p;
     +		status = strchr(p, ' ');
     +		if (!status) {
    -+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'",
    ++			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
     +				    reader->line);
    -+			return -1;
    ++			code = -1;
    ++			continue;
     +		}
     +		*status++ = '\0';
     +		if (strlen(status) > 2 && *(status + 2) == ' ') {
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			*msg++ = '\0';
     +		}
     +		if (strlen(status) != 2) {
    -+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
    ++			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
     +				    status, reader->line);
    -+			return -1;
    ++			code = -1;
    ++			continue;
     +		}
     +
     +		/* first try searching at our hint, falling back to all refs */
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +				    refname);
     +			continue;
     +		}
    -+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
    -+		oidcpy(&hint->old_oid, &old_oid);
    -+		oidcpy(&hint->new_oid, &new_oid);
     +		if (!strcmp(status, "ng")) {
     +			if (msg)
     +				hint->error_string = xstrdup(msg);
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +				hint->error_string = "failed";
     +			code = -1;
     +		} else if (strcmp("ok", status)) {
    -+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
    ++			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
     +				    status, reader->line);
    -+			return -1;
    ++			code = -1;
    ++			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
    ++			continue;
     +		}
    ++		oidcpy(&hint->old_oid, &old_oid);
    ++		oidcpy(&hint->new_oid, &new_oid);
    ++		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
     +	}
     +
     +	for (cmd = commands; cmd; cmd = cmd->next)
    @@ t/helper/test-proc-receive.c (new)
     +#include "connect.h"
     +#include "parse-options.h"
     +#include "pkt-line.h"
    ++#include "sigchain.h"
     +#include "string-list.h"
     +#include "test-tool.h"
     +
    @@ t/helper/test-proc-receive.c (new)
     +			   PACKET_READ_CHOMP_NEWLINE |
     +			   PACKET_READ_DIE_ON_ERR_PACKET);
     +
    ++	sigchain_push(SIGPIPE, SIG_IGN);
     +	proc_receive_verison(&reader);
     +	proc_receive_read_commands(&reader, &commands);
     +	proc_receive_read_push_options(&reader, &push_options);
    @@ t/helper/test-proc-receive.c (new)
     +			packet_write_fmt(1, "%s\n", item->string);
     +	}
     +	packet_flush(1);
    ++	sigchain_pop(SIGPIPE);
     +
     +	return 0;
     +}

3:  230f28198f = 3:  cde556e9c7 refs.c: refactor to reuse ref_is_hidden()

4:  e6a7608a84 ! 4:  3200327695 receive-pack: new config receive.procReceiveRefs
    @@ Documentation/config/receive.txt: receive.hideRefs::
     +	used, and all commands will be executed by the internal
     +	`execute_commands` function.
     +
    -+	For example, if this variable is set to "refs/for/", pushing to
    ++	For example, if this variable is set to "refs/for", pushing to
     +	reference such as "refs/for/master" will not create or update a
     +	reference named "refs/for/master", but may create or update a
     +	pull request directly by running an external hook.
     
5:  e426775925 ! 5:  5f2ab02b01 receive-pack: refactor report for proc-receive
    @@ builtin/receive-pack.c: static int read_proc_receive_result(struct packet_reader
     +			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
     +			hint->run_proc_receive = 0;
     +		} else {
    - 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'",
    + 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
      				    status, reader->line);
    - 			return -1;
    + 			code = -1;
    @@ builtin/receive-pack.c: static int read_proc_receive_result(struct packet_reader *reader,
    + 		}
    + 		oidcpy(&hint->old_oid, &old_oid);
    + 		oidcpy(&hint->new_oid, &new_oid);
    +-		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
    ++		if (hint->run_proc_receive)
    ++			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
    + 	}
    + 
    + 	for (cmd = commands; cmd; cmd = cmd->next)
    @@ transport.c: void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
    - 	}
    - }
    - 
    --static void print_ref_status(char flag, const char *summary,
    -+static void print_ref_status(char flag, const char *summary, char *target_refname,
    +@@ transport.c: static void print_ref_status(char flag, const char *summary,
      			     struct ref *to, struct ref *from, const char *msg,
      			     int porcelain, int summary_width)
      {
    -+	if (!target_refname)
    -+		target_refname = to->name;
    ++	char *from_name = NULL;
    ++	char *to_name = NULL;
    ++
    ++	if (from) {
    ++		if (from->remote_status && !strncmp(from->remote_status, "ref:", 4))
    ++			from_name = from->remote_status + 4;
    ++		else
    ++			from_name = from->name;
    ++	}
    ++
    ++	if (to) {
    ++		if (to->remote_status && !strncmp(to->remote_status, "ref:", 4))
    ++			to_name = to->remote_status + 4;
    ++		else
    ++			to_name = to->name;
    ++	}
     +
      	if (porcelain) {
      		if (from)
     -			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
    -+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, target_refname);
    ++			fprintf(stdout, "%c\t%s:%s\t", flag, from_name, to_name);
      		else
     -			fprintf(stdout, "%c\t:%s\t", flag, to->name);
    -+			fprintf(stdout, "%c\t:%s\t", flag, target_refname);
    ++			fprintf(stdout, "%c\t:%s\t", flag, to_name);
      		if (msg)
      			fprintf(stdout, "%s (%s)\n", summary, msg);
      		else
    @@ transport.c: static void print_ref_status(char flag, const char *summary,
      			summary, reset);
      		if (from)
     -			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
    -+			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(target_refname));
    ++			fprintf(stderr, "%s -> %s", prettify_refname(from_name), prettify_refname(to_name));
      		else
     -			fputs(prettify_refname(to->name), stderr);
    -+			fputs(prettify_refname(target_refname), stderr);
    ++			fputs(prettify_refname(to_name), stderr);
      		if (msg) {
      			fputs(" (", stderr);
      			fputs(msg, stderr);
    -@@ transport.c: static void print_ref_status(char flag, const char *summary,
    - 
    - static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
    - {
    -+	char *refname;
    -+
    -+	if (ref->remote_status && !strncmp(ref->remote_status, "ref:", 4))
    -+		refname = ref->remote_status + 4;
    -+	else
    -+		refname = ref->name;
    -+
    - 	if (ref->deletion)
    --		print_ref_status('-', "[deleted]", ref, NULL, NULL,
    -+		print_ref_status('-', "[deleted]", refname, ref, NULL, NULL,
    - 				 porcelain, summary_width);
    --	else if (is_null_oid(&ref->old_oid))
    -+	else if (is_null_oid(&ref->old_oid)) {
    -+
    - 		print_ref_status('*',
    --				 (starts_with(ref->name, "refs/tags/")
    -+				 (starts_with(refname, "refs/tags/")
    - 				  ? "[new tag]"
    --				  : (starts_with(ref->name, "refs/heads/")
    -+				  : (starts_with(refname, "refs/heads/")
    - 				     ? "[new branch]"
    - 				     : "[new reference]")),
    --				 ref, ref->peer_ref, NULL, porcelain, summary_width);
    --	else {
    -+				 refname, ref, ref->peer_ref, NULL, porcelain, summary_width);
    -+	} else {
    - 		struct strbuf quickref = STRBUF_INIT;
    - 		char type;
    - 		const char *msg;
    -@@ transport.c: static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
    - 		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
    - 					 DEFAULT_ABBREV);
    - 
    --		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
    -+		print_ref_status(type, quickref.buf, refname, ref, ref->peer_ref, msg,
    - 				 porcelain, summary_width);
    - 		strbuf_release(&quickref);
    - 	}
    -@@ transport.c: static int print_one_push_status(struct ref *ref, const char *dest, int count,
    - 
    - 	switch(ref->status) {
    - 	case REF_STATUS_NONE:
    --		print_ref_status('X', "[no match]", ref, NULL, NULL,
    -+		print_ref_status('X', "[no match]", NULL, ref, NULL, NULL,
    - 				 porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REJECT_NODELETE:
    --		print_ref_status('!', "[rejected]", ref, NULL,
    -+		print_ref_status('!', "[rejected]", NULL, ref, NULL,
    - 				 "remote does not support deleting refs",
    - 				 porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_UPTODATE:
    --		print_ref_status('=', "[up to date]", ref,
    -+		print_ref_status('=', "[up to date]", NULL, ref,
    - 				 ref->peer_ref, NULL, porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REJECT_NONFASTFORWARD:
    --		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
    -+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
    - 				 "non-fast-forward", porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REJECT_ALREADY_EXISTS:
    --		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
    -+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
    - 				 "already exists", porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REJECT_FETCH_FIRST:
    --		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
    -+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
    - 				 "fetch first", porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REJECT_NEEDS_FORCE:
    --		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
    -+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
    - 				 "needs force", porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REJECT_STALE:
    --		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
    -+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
    - 				 "stale info", porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REJECT_SHALLOW:
    --		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
    -+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
    - 				 "new shallow roots not allowed",
    - 				 porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_REMOTE_REJECT:
    --		print_ref_status('!', "[remote rejected]", ref,
    -+		print_ref_status('!', "[remote rejected]", NULL, ref,
    - 				 ref->deletion ? NULL : ref->peer_ref,
    - 				 ref->remote_status, porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_EXPECTING_REPORT:
    --		print_ref_status('!', "[remote failure]", ref,
    -+		print_ref_status('!', "[remote failure]", NULL, ref,
    - 				 ref->deletion ? NULL : ref->peer_ref,
    - 				 "remote failed to report status",
    - 				 porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_ATOMIC_PUSH_FAILED:
    --		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
    -+		print_ref_status('!', "[rejected]", NULL, ref, ref->peer_ref,
    - 				 "atomic push failed", porcelain, summary_width);
    - 		break;
    - 	case REF_STATUS_OK:

6:  c5982067be < -:  ---------- t5412: test the proc-receive hook on HTTP protocol

7:  823b7f2ea6 = 6:  b7d7175d89 doc: add documentation for the proc-receive hook

---

Jiang Xin (6):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  receive-pack: refactor report for proc-receive
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt           |  14 +
 Documentation/githooks.txt                 |  70 ++
 Makefile                                   |   1 +
 builtin/receive-pack.c                     | 318 +++++++-
 refs.c                                     |  11 +-
 refs.h                                     |   1 +
 t/helper/test-proc-receive.c               | 175 +++++
 t/helper/test-tool.c                       |   1 +
 t/helper/test-tool.h                       |   1 +
 t/t5411-proc-receive-hook.sh               |  75 ++
 t/t5411/common-functions.sh                |  53 ++
 t/t5411/common-test-cases.sh               | 827 +++++++++++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh |  86 +++
 t/t5516-fetch-push.sh                      |   2 +-
 transport-helper.c                         |  64 +-
 transport.c                                |  34 +-
 16 files changed, 1684 insertions(+), 49 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v9 1/6] transport: not report a non-head push as a branch
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
@ 2020-04-07 12:08         ` Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 2/6] receive-pack: add new proc-receive hook Jiang Xin
                           ` (4 subsequent siblings)
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-07 12:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh               | 75 +++++++++++++++++++
 t/t5411/common-functions.sh                | 49 ++++++++++++
 t/t5411/common-test-cases.sh               | 43 +++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh | 86 ++++++++++++++++++++++
 t/t5516-fetch-push.sh                      |  2 +-
 transport.c                                |  9 ++-
 6 files changed, 260 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..09d516cf1b
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace URL of different protocol for the upstream repository to
+# a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common | sed \
+		-e "s#To ../upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream.git/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream.git/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream.git/hooks/pre-receive \
+		upstream.git/hooks/post-receive &&
+
+	upstream=upstream.git
+'
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..e46884f74d
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,49 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.
+make_user_friendly_and_stable_output_common () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+}
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
new file mode 100644
index 0000000000..33b8541f28
--- /dev/null
+++ b/t/t5411/common-test-cases.sh
@@ -0,0 +1,43 @@
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
new file mode 100755
index 0000000000..4883e69299
--- /dev/null
+++ b/t/t5412-proc-receive-hook-http-protocol.sh
@@ -0,0 +1,86 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook for HTTP protocol'
+
+. ./test-lib.sh
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace URL of different protocol for the upstream repository to
+# a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common | sed \
+		-e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git -C upstream.git config http.receivepack true &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream.git/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream.git/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream.git/hooks/pre-receive \
+		upstream.git/hooks/post-receive &&
+
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..272c0f4046 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v9 2/6] receive-pack: add new proc-receive hook
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 1/6] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-07 12:08         ` Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 3/6] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                           ` (3 subsequent siblings)
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-07 12:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and capabilities negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(old-oid new-oid ref)
    S: ... ...
    S: flush-pkt
    # Only if push-options have been negotiated.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(old-oid new-oid ref ok)
    # NO, I reject it.
    H: PKT-LINE(old-oid new-oid ref ng reason)
    # OK, but use an alternate reference. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
    # It will fallthrough to receive-pack to execute. (in latter commit)
    H: PKT-LINE(old-oid new-oid ref ft)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook can create/update another alternate
reference.  For example, a command for a reference "refs/for/master" may
create a special reference, such as "refs/pull/123/head".  The alternate
reference can be returned from the result in an extensible format like
"<old-oid> <new-oid> <reference> <status> [<message>]".

The result will be stored in a command list, and "receive-pack" will use
the result to replace the commands that have specific `run_proc_receive`
field turned on.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 269 +++++++++++++++-
 t/helper/test-proc-receive.c | 175 +++++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411/common-functions.sh  |  10 +-
 t/t5411/common-test-cases.sh | 576 +++++++++++++++++++++++++++++++++++
 7 files changed, 1027 insertions(+), 6 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..f3a2c2612d 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +820,239 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for ( ; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_result(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *hint;
+	struct command *cmd;
+	int code = 0;
+
+	hint = NULL;
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ') {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status) {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+		*status++ = '\0';
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
+				    status, reader->line);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on ref of builtin command: %s\n",
+				    refname);
+			continue;
+		}
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				hint->error_string = xstrdup(msg);
+			else
+				hint->error_string = "failed";
+			code = -1;
+		} else if (strcmp("ok", status)) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
+				    status, reader->line);
+			code = -1;
+			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
+			continue;
+		}
+		oidcpy(&hint->old_oid, &old_oid);
+		oidcpy(&hint->new_oid, &new_oid);
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "no report from proc-receive";
+		    code = -1;
+		}
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		char *old_hex, *new_hex;
+
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		old_hex = oid_to_hex(&cmd->old_oid);
+		new_hex = oid_to_hex(&cmd->new_oid);
+		packet_write_fmt(proc.in, "%s %s %s",
+				 old_hex, new_hex, cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_result(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1628,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1668,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1704,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1734,20 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1774,18 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive) {
+		int code;
+
+		code = run_proc_receive_hook(commands, push_options);
+		if (code) {
+			for (cmd = commands; cmd; cmd = cmd->next) {
+				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+					cmd->error_string = "fail to run proc-receive hook";
+			}
+		}
+	}
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..ca44b05bd0
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,175 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			break;
+		}
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			char *old_hex, *new_hex;
+
+			old_hex = oid_to_hex(&cmd->old_oid);
+			new_hex = oid_to_hex(&cmd->new_oid);
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				old_hex, new_hex, cmd->ref_name);
+		}
+
+		if (push_options.nr > 0) {
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+		}
+
+		if (returns.nr) {
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+		}
+	}
+
+	if (returns.nr) {
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	}
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index e46884f74d..6ccab42c6c 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -34,8 +34,11 @@ create_commits_in () {
 # Format the output of git-push, git-show-ref and other commands to make a
 # user-friendly and stable text.  We can easily prepare the expect text
 # without having to worry about future changes of the commit ID and spaces
-# of the output.  We also replce single quotes with double quotes, because
-# it is boring to prepare unquoted single quotes in expect txt.
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
 make_user_friendly_and_stable_output_common () {
 	sed \
 		-e "s/  *\$//" \
@@ -45,5 +48,6 @@ make_user_friendly_and_stable_output_common () {
 		-e "s/$B/<COMMIT-B>/g" \
 		-e "s/$TAG/<TAG-v123>/g" \
 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
-		-e "s/[0-9a-f]\{7,\}/<OID>/g"
+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
+		-e "/^error: / d"
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 33b8541f28..08c7abf11a 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -41,3 +41,579 @@ test_expect_success "normal git-push command" '
 	EOF
 	test_cmp expect actual
 '
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { s/   */ /g; p; }" \
+		-e "/^ / { s/   */ /g; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	sed -n -e "/^remote: error:/ { s/   */ /g; p; }" \
+		<actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v9 3/6] refs.c: refactor to reuse ref_is_hidden()
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
                           ` (2 preceding siblings ...)
  2020-04-07 12:08         ` [PATCH v9 2/6] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-07 12:08         ` Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 4/6] receive-pack: new config receive.procReceiveRefs Jiang Xin
                           ` (2 subsequent siblings)
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-07 12:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v9 4/6] receive-pack: new config receive.procReceiveRefs
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
                           ` (3 preceding siblings ...)
  2020-04-07 12:08         ` [PATCH v9 3/6] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-07 12:08         ` Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 5/6] receive-pack: refactor report for proc-receive Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 6/6] doc: add documentation for the proc-receive hook Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-07 12:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for/
    git config --system --add receive.procReceiveRefs refs/drafts/

If the specific prefix strings match the reference names of the commands
which are sent by git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for/", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the external
hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 ++++
 builtin/receive-pack.c           |  43 ++++++++--
 t/t5411/common-test-cases.sh     | 130 +++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..4f993ee376 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hooks "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running an external hook.
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index f3a2c2612d..a78daa6733 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1737,15 +1752,26 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2205,6 +2231,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2320,5 +2348,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 08c7abf11a..a9e80c374b 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -53,6 +53,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -617,3 +625,125 @@ test_expect_success "push with options" '
 	EOF
 	test_cmp expect actual
 '
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v9 5/6] receive-pack: refactor report for proc-receive
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
                           ` (4 preceding siblings ...)
  2020-04-07 12:08         ` [PATCH v9 4/6] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-07 12:08         ` Jiang Xin
  2020-04-07 12:08         ` [PATCH v9 6/6] doc: add documentation for the proc-receive hook Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-07 12:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The "proc-receive" may update one or more references, and will send its
result one by one in pkt-line format.  Each line of the result has four
fields and one optional message field, as "<old-oid> <new-oid> <ref>
<status> [<message>]".  See the following example:

    # OK, run this command successfully.
    PKT-LINE(old-oid new-oid ref ok)

    # NO, I reject it.
    PKT-LINE(old-oid new-oid ref ng reason)

    # OK, but use an alternate reference.
    PKT-LINE(old-oid new-oid ref ok ref:alt-ref)

    # It will fallthrough to receive-pack to execute.
    PKT-LINE(old-oid new-oid ref ft)

The first three fields have the same foramt as a command.

The forth field has a two-letter status code.  Available status code:

* ok: The command runs successfully.  If the optional message has a
  prefix "ref:", the hook has created/updated an alternate reference
  instead.

* ng: Fail to run the command. Error message is in the optional message
  field.

* ft: Will fallthrough to receive-pack to execute.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 24 +++++++---
 t/t5411/common-test-cases.sh | 90 +++++++++++++++++++++++++++++++++---
 transport-helper.c           | 64 ++++++++++++-------------
 transport.c                  | 25 ++++++++--
 4 files changed, 156 insertions(+), 47 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a78daa6733..73acb1b5c8 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -328,6 +328,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extra_string;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     run_proc_receive:2;
@@ -913,7 +914,12 @@ static int read_proc_receive_result(struct packet_reader *reader,
 			else
 				hint->error_string = "failed";
 			code = -1;
-		} else if (strcmp("ok", status)) {
+		} else if (!strcmp("ok", status)) {
+			hint->extra_string = xstrdup_or_null(msg);
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else {
 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
 				    status, reader->line);
 			code = -1;
@@ -922,7 +928,8 @@ static int read_proc_receive_result(struct packet_reader *reader,
 		}
 		oidcpy(&hint->old_oid, &old_oid);
 		oidcpy(&hint->new_oid, &new_oid);
-		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next)
@@ -2188,12 +2195,17 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
+		if (!cmd->error_string) {
+			if (!cmd->extra_string)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			else
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, ' ', cmd->extra_string);
+		} else {
 			packet_buf_write(&buf, "ng %s %s\n",
 					 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index a9e80c374b..a2a16c0ed4 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -640,8 +640,8 @@ test_expect_success "setup proc-receive hook" '
 
 	test-tool proc-receive -v \
 		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
-		-r "$ZERO_OID $A refs/for/next/topic ok" \
-		-r "$ZERO_OID $A refs/for/master/topic ok"
+		-r "$ZERO_OID $A refs/for/next/topic ok ref:refs/pull/123/head" \
+		-r "$ZERO_OID $A refs/for/master/topic ok ref:refs/pull/124/head"
 	EOF
 	chmod a+x "$upstream/hooks/proc-receive"
 '
@@ -666,16 +666,16 @@ test_expect_success "report update of all special refs" '
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok ref:refs/pull/124/head
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/review/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -704,6 +704,7 @@ test_expect_success "setup proc-receive hook" '
 # git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
 test_expect_success "report mixed refs update" '
 	git -C workbench push origin \
+		$B:refs/heads/master \
 		HEAD:refs/heads/bar \
 		HEAD:refs/heads/baz \
 		HEAD:refs/for/next/topic \
@@ -713,6 +714,7 @@ test_expect_success "report mixed refs update" '
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
@@ -724,12 +726,14 @@ test_expect_success "report mixed refs update" '
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
 	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
@@ -743,6 +747,80 @@ test_expect_success "report mixed refs update" '
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all ref/" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok ref:refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok ref:refs/pull/124/head"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      :                       (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok ref:refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok ref:refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID>..<OID> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID>...<OID> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/pull/123/head
+	 * [new reference] HEAD -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/master
 	EOF
 	test_cmp expect actual
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..cec3495d59 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
diff --git a/transport.c b/transport.c
index 272c0f4046..97b7c6a442 100644
--- a/transport.c
+++ b/transport.c
@@ -463,11 +463,28 @@ static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	char *from_name = NULL;
+	char *to_name = NULL;
+
+	if (from) {
+		if (from->remote_status && !strncmp(from->remote_status, "ref:", 4))
+			from_name = from->remote_status + 4;
+		else
+			from_name = from->name;
+	}
+
+	if (to) {
+		if (to->remote_status && !strncmp(to->remote_status, "ref:", 4))
+			to_name = to->remote_status + 4;
+		else
+			to_name = to->name;
+	}
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from_name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +498,9 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s", prettify_refname(from_name), prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v9 6/6] doc: add documentation for the proc-receive hook
  2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
                           ` (5 preceding siblings ...)
  2020-04-07 12:08         ` [PATCH v9 5/6] receive-pack: refactor report for proc-receive Jiang Xin
@ 2020-04-07 12:08         ` Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-07 12:08 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 70 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..10ea5c1f18 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,76 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+special `git push` command.  According to refnames of the commands which
+`git push` sends to 'git-receive-pack', the commands will be devided
+into two groups by matching what the `receive.procReceiveRefs`
+configuration variable defines.  One group of the commands will execute
+the internal `execute_commands` function to update the corresponding
+refnames, and the other group of commands which have matching refnames
+will execute this 'proc-receive' hook to create pull requests, etc.
+If there is no `receive.procReceiveRefs` settings, this hook won't
+execute at all, and all commands are sent to the internal
+`execute_commands` function.
+
+Its exit status only determines the success or failure of the group of
+commands with special refnames, unless atomic push is in use.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but will talk a protocol in pkt-line format with the
+'receive-pack' for reading commands, push-options (optional), and
+sending result.  In the following example, The letter "S" stands for
+"receive-pack" and letter "H" stands for the hook.
+
+    # Version and capabilities negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(old-oid new-oid ref)
+    S: ... ...
+    S: flush-pkt
+    # Only if push-options have been negotiated.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(old-oid new-oid ref ok)
+    # NO, I reject it.
+    H: PKT-LINE(old-oid new-oid ref ng reason)
+    # OK, but use an alternate reference.
+    H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
+    # It will fallthrough to receive-pack to execute.
+    H: PKT-LINE(old-oid new-oid ref ft)
+    H: ... ...
+    H: flush-pkt
+
+The "proc-receive" hook may update one or more references, and will send
+its result one by one in pkt-line format.  Each line of the result has
+four fields and one optional message field, like "<old-oid> <new-oid>
+<ref> <status> [<message>]".
+
+The first three fields are the same as those of the commands for
+"receive-pack".
+
+The forth field has a two-letter status code.  Available status codes:
+
+* ok: The command runs successfully.  If the optional message has a
+  prefix "ref:", the hook has created/updated an alternate reference
+  instead.
+
+* ng: Fail to run the command.  Error message is given in the optional
+  message field.
+
+* ft: Will fallthrough to receive-pack to execute.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 0/8] New proc-receive hook for centralized workflow
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 0/7] " Jiang Xin
                               ` (7 more replies)
  2020-04-12 13:30           ` [PATCH v10 1/8] transport: not report a non-head push as a branch Jiang Xin
                             ` (7 subsequent siblings)
  8 siblings, 8 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>


## Changes since v9

1. Minor refactors on patch 1/8, 2/8, 4/8, and 8/8.

2. New patch 5/8: export function `parse_feature_value()`.

3. Split patch 5/6 of reroll v9 into two patches (6/8 and 7/8),
   one for the server-side and one for the client-side.
   so can make a compatible test on old version of a Git client.

4. Patch 6/8: Use null character to split extended status of the report
   line, and add other key-value pairs for extension. For example:

   When we push to a pseudo reference using the following command:

       git push origin HEAD:refs/for/master/topic

   Even if the push command updates an existing pull request (such as
   refs/pull/123/head) instead of creating a new one, `git push`
   always give the following report:

       To <URL/of/upstream.git>
         * [new reference]      HEAD -> refs/pull/123/head

   After add more key-value pairs in the extended status, such as:
   'old-oid=<...>', 'forced-update', we get a nicer status report like:

       To upstream.git
        + 1234567...7654321     HEAD -> refs/pull/124/head (forced update)


## Range-diff v9...v10

1:  ba6222899b ! 1:  a293077bcf transport: not report a non-head push as a branch
    @@ t/t5411-proc-receive-hook.sh (new)
     +
     +# Format the output of git-push, git-show-ref and other commands to make a
     +# user-friendly and stable text.  In addition to the common format method,
    -+# we also replace URL of different protocol for the upstream repository to
    -+# a fixed pattern.
    ++# we also replace the URL of different protocol for the upstream repository
    ++# with a fixed pattern.
     +make_user_friendly_and_stable_output () {
     +	make_user_friendly_and_stable_output_common | sed \
     +		-e "s#To ../upstream.git#To <URL/of/upstream.git>#"
    @@ t/t5411/common-functions.sh (new)
     +		-e "s/$B/<COMMIT-B>/g" \
     +		-e "s/$TAG/<TAG-v123>/g" \
     +		-e "s/$ZERO_OID/<ZERO-OID>/g" \
    -+		-e "s/[0-9a-f]\{7,\}/<OID>/g"
    ++		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
    ++		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
     +}
     
      ## t/t5411/common-test-cases.sh (new) ##
    @@ t/t5411/common-test-cases.sh (new)
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
     +	To <URL/of/upstream.git>
    -+	 + <OID>...<OID> HEAD -> master (forced update)
    ++	 + <OID-B>...<OID-A> HEAD -> master (forced update)
     +	 - [deleted] next
     +	 * [new tag] v123 -> v123
     +	 * [new reference] HEAD -> refs/review/master/topic
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +
     +# Format the output of git-push, git-show-ref and other commands to make a
     +# user-friendly and stable text.  In addition to the common format method,
    -+# we also replace URL of different protocol for the upstream repository to
    -+# a fixed pattern.
    ++# we also replace the URL of different protocol for the upstream repository
    ++# with a fixed pattern.
     +make_user_friendly_and_stable_output () {
     +	make_user_friendly_and_stable_output_common | sed \
     +		-e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
2:  195c5b0a0c ! 2:  915b9ef616 receive-pack: add new proc-receive hook
    @@ Commit message
         format.  In the following example, The letter "S" stands for
         "receive-pack" and letter "H" stands for the hook.
     
    -        # Version and capabilities negotiation.
    +        # Version and features negotiation.
             S: PKT-LINE(version=1\0push-options atomic...)
             S: flush-pkt
             H: PKT-LINE(version=1\0push-options...)
             H: flush-pkt
     
             # Send commands from server to the hook.
    -        S: PKT-LINE(old-oid new-oid ref)
    +        S: PKT-LINE(<old-oid> <new-oid> <ref>)
             S: ... ...
             S: flush-pkt
    -        # Only if push-options have been negotiated.
    +        # Send push-options only if the 'push-options' feature is enabled.
             S: PKT-LINE(push-option)
             S: ... ...
             S: flush-pkt
     
             # Receive result from the hook.
             # OK, run this command successfully.
    -        H: PKT-LINE(old-oid new-oid ref ok)
    +        H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
             # NO, I reject it.
    -        H: PKT-LINE(old-oid new-oid ref ng reason)
    -        # OK, but use an alternate reference. (in latter commit)
    -        H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref)
    -        # It will fallthrough to receive-pack to execute. (in latter commit)
    -        H: PKT-LINE(old-oid new-oid ref ft)
    +        H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
    +        # Fall through, let 'receive-pack' to execute it.
    +        H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
    +        # OK, but has an alternate reference.  The alternate reference name
    +        # and other status are given in key=value pairs after the null
    +        # character.
    +        H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
    +                    forced-update)
             H: ... ...
             H: flush-pkt
     
    -    After receiving a command, the hook can create/update another alternate
    -    reference.  For example, a command for a reference "refs/for/master" may
    -    create a special reference, such as "refs/pull/123/head".  The alternate
    -    reference can be returned from the result in an extensible format like
    -    "<old-oid> <new-oid> <reference> <status> [<message>]".
    +    After receiving a command, the hook will execute the command, and may
    +    create/update different reference.  For example, a command for a pseudo
    +    reference "refs/for/master/topic" may create/update different reference
    +    such as "refs/pull/123/head".  The alternate reference name and other
    +    status are given in key-value pairs as extended status of the report
    +    line.
     
    -    The result will be stored in a command list, and "receive-pack" will use
    -    the result to replace the commands that have specific `run_proc_receive`
    -    field turned on.
    +    The list of commands returned from "proc-receive" will replace the
    +    relevant commands that are sent from user to "receive-pack", and
    +    "receive-pack" will continue to run the "execute_commands" function and
    +    other routines.  Finally, the result of the execution of these commands
    +    will be reported to end user.
     
         Suggested-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +static struct command *find_command_by_refname(const struct command *list,
     +					       const char *refname)
     +{
    -+	for ( ; list; list = list->next)
    ++	for (; list; list = list->next)
     +		if (!strcmp(list->ref_name, refname))
     +			return (struct command *)list;
     +	return NULL;
     +}
     +
    -+static int read_proc_receive_result(struct packet_reader *reader,
    ++static int read_proc_receive_report(struct packet_reader *reader,
     +				    struct command *commands,
     +				    struct strbuf *errmsg)
     +{
    -+	struct command *hint;
     +	struct command *cmd;
    ++	struct command *hint = NULL;
     +	int code = 0;
     +
    -+	hint = NULL;
     +	for (;;) {
     +		struct object_id old_oid, new_oid;
     +		const char *refname;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	}
     +
     +	for (cmd = commands; cmd; cmd = cmd->next)
    -+		if (cmd->run_proc_receive &&
    ++		if (cmd->run_proc_receive && !cmd->error_string &&
     +		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
     +		    cmd->error_string = "no report from proc-receive";
     +		    code = -1;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +	/* Send commands */
     +	for (cmd = commands; cmd; cmd = cmd->next) {
    -+		char *old_hex, *new_hex;
    -+
     +		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
     +			continue;
    -+		old_hex = oid_to_hex(&cmd->old_oid);
    -+		new_hex = oid_to_hex(&cmd->new_oid);
     +		packet_write_fmt(proc.in, "%s %s %s",
    -+				 old_hex, new_hex, cmd->ref_name);
    ++				 oid_to_hex(&cmd->old_oid),
    ++				 oid_to_hex(&cmd->new_oid),
    ++				 cmd->ref_name);
     +	}
     +	packet_flush(proc.in);
     +
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	}
     +
     +	/* Read result from proc-receive */
    -+	code = read_proc_receive_result(&reader, commands, &errmsg);
    ++	code = read_proc_receive_report(&reader, commands, &errmsg);
     +
     +cleanup:
     +	close(proc.in);
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      	free(head_name_to_free);
      	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
      
    -+	if (run_proc_receive) {
    -+		int code;
    -+
    -+		code = run_proc_receive_hook(commands, push_options);
    -+		if (code) {
    -+			for (cmd = commands; cmd; cmd = cmd->next) {
    -+				if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
    -+					cmd->error_string = "fail to run proc-receive hook";
    -+			}
    -+		}
    -+	}
    ++	if (run_proc_receive &&
    ++	    run_proc_receive_hook(commands, push_options))
    ++		for (cmd = commands; cmd; cmd = cmd->next)
    ++			if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
    ++				cmd->error_string = "fail to run proc-receive hook";
     +
      	if (use_atomic)
      		execute_commands_atomic(commands, si);
    @@ t/helper/test-proc-receive.c (new)
     +		const char *refname;
     +		const char *p;
     +
    -+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
    ++		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
     +			break;
    -+		}
     +
     +		if (parse_oid_hex(reader->line, &old_oid, &p) ||
     +		    *p++ != ' ' ||
    @@ t/helper/test-proc-receive.c (new)
     +int cmd__proc_receive(int argc, const char **argv)
     +{
     +	struct packet_reader reader;
    -+	struct command *commands;
    ++	struct command *commands = NULL;
     +	struct string_list push_options = STRING_LIST_INIT_DUP;
     +	struct string_list_item *item;
     +	struct option options[] = {
    @@ t/helper/test-proc-receive.c (new)
     +				use_atomic? " atomic": "",
     +				use_push_options ? " push_options": "");
     +
    -+		for (cmd = commands; cmd; cmd = cmd->next) {
    -+			char *old_hex, *new_hex;
    -+
    -+			old_hex = oid_to_hex(&cmd->old_oid);
    -+			new_hex = oid_to_hex(&cmd->new_oid);
    ++		for (cmd = commands; cmd; cmd = cmd->next)
     +			fprintf(stderr, "proc-receive< %s %s %s\n",
    -+				old_hex, new_hex, cmd->ref_name);
    -+		}
    ++				oid_to_hex(&cmd->old_oid),
    ++				oid_to_hex(&cmd->new_oid),
    ++				cmd->ref_name);
     +
    -+		if (push_options.nr > 0) {
    ++		if (push_options.nr > 0)
     +			for_each_string_list_item(item, &push_options)
     +				fprintf(stderr, "proc-receive< %s\n", item->string);
    -+		}
     +
    -+		if (returns.nr) {
    ++		if (returns.nr)
     +			for_each_string_list_item(item, &returns)
     +				fprintf(stderr, "proc-receive> %s\n", item->string);
    -+		}
     +	}
     +
    -+	if (returns.nr) {
    ++	if (returns.nr)
     +		for_each_string_list_item(item, &returns)
     +			packet_write_fmt(1, "%s\n", item->string);
    -+	}
     +	packet_flush(1);
     +	sigchain_pop(SIGPIPE);
     +
    @@ t/t5411/common-functions.sh: create_commits_in () {
      	sed \
      		-e "s/  *\$//" \
     @@ t/t5411/common-functions.sh: make_user_friendly_and_stable_output_common () {
    - 		-e "s/$B/<COMMIT-B>/g" \
      		-e "s/$TAG/<TAG-v123>/g" \
      		-e "s/$ZERO_OID/<ZERO-OID>/g" \
    --		-e "s/[0-9a-f]\{7,\}/<OID>/g"
    -+		-e "s/[0-9a-f]\{7,\}/<OID>/g" \
    + 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
    +-		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
    ++		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
     +		-e "/^error: / d"
      }
     
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	To <URL/of/upstream.git>
    -+	 <OID>..<OID> <COMMIT-B> -> master
    ++	 <OID-A>..<OID-B> <COMMIT-B> -> master
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
     +	EOF
     +	test_cmp expect actual &&
3:  cde556e9c7 = 3:  16257616ec refs.c: refactor to reuse ref_is_hidden()
4:  3200327695 ! 4:  cd49b57c17 receive-pack: new config receive.procReceiveRefs
    @@ Commit message
         Add a new multi-valued config variable "receive.procReceiveRefs"
         for `receive-pack` command, like the follows:
     
    -        git config --system --add receive.procReceiveRefs refs/for/
    -        git config --system --add receive.procReceiveRefs refs/drafts/
    +        git config --system --add receive.procReceiveRefs refs/for
    +        git config --system --add receive.procReceiveRefs refs/drafts
     
         If the specific prefix strings match the reference names of the commands
    -    which are sent by git client to `receive-pack`, these commands will be
    +    which are sent from git client to `receive-pack`, these commands will be
         executed by an external hook (named "proc-receive"), instead of the
         internal `execute_commands` function.
     
    -    For example, if it is set to "refs/for/", pushing to a reference such as
    +    For example, if it is set to "refs/for", pushing to a reference such as
         "refs/for/master" will not create or update reference "refs/for/master",
    -    but may create or update a pull request directly by running the external
    -    hook.
    +    but may create or update a pull request directly by running the hook
    +    "proc-receive".
     
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
    @@ Documentation/config/receive.txt: receive.hideRefs::
     +receive.procReceiveRefs::
     +	This is a multi-valued variable that defines reference prefixes
     +	to match the commands in `receive-pack`.  Commands matching the
    -+	prefixes will be executed by an external hooks "proc-receive",
    ++	prefixes will be executed by an external hook "proc-receive",
     +	instead of the internal `execute_commands` function.  If this
     +	variable is not defined, the "proc-receive" hook will never be
     +	used, and all commands will be executed by the internal
    @@ Documentation/config/receive.txt: receive.hideRefs::
     +	For example, if this variable is set to "refs/for", pushing to
     +	reference such as "refs/for/master" will not create or update a
     +	reference named "refs/for/master", but may create or update a
    -+	pull request directly by running an external hook.
    ++	pull request directly by running the hook "proc-receive".
     +
      receive.updateServerInfo::
      	If set to true, git-receive-pack will run git-update-server-info
    @@ t/t5411/common-test-cases.sh: test_expect_success "push with options" '
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
     +		-r "$ZERO_OID $A refs/for/next/topic ok" \
    -+		-r "$ZERO_OID $A refs/for/master/topic ok"
    ++		-r "$B $A refs/for/master/topic ok"
     +	EOF
     +	chmod a+x "$upstream/hooks/proc-receive"
     +'
    @@ t/t5411/common-test-cases.sh: test_expect_success "push with options" '
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    ++	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 * [new reference] HEAD -> refs/for/next/topic
     +	 * [new reference] HEAD -> refs/review/a/b/c/topic
    @@ t/t5411/common-test-cases.sh: test_expect_success "push with options" '
     +
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/for/next/topic ok" \
    -+		-r "$ZERO_OID $A refs/for/master/topic ok"
    ++		-r "$A $B refs/for/master/topic ok"
     +	EOF
     +	chmod a+x "$upstream/hooks/proc-receive"
     +'
    @@ t/t5411/common-test-cases.sh: test_expect_success "push with options" '
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    ++	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 * [new branch] HEAD -> bar
     +	 * [new branch] HEAD -> baz
5:  5f2ab02b01 < -:  ---------- receive-pack: refactor report for proc-receive
6:  b7d7175d89 < -:  ---------- doc: add documentation for the proc-receive hook
-:  ---------- > 5:  75b178fc19 connect: export parse_feature_value()
-:  ---------- > 6:  860860536e receive-pack: extension for server-side report
-:  ---------- > 7:  d1677b61d1 send-pack: extension for client-side status report
-:  ---------- > 8:  41159c2ef2 doc: add documentation for the proc-receive hook


Jiang Xin (8):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  connect: export parse_feature_value()
  receive-pack: extension for server-side report
  send-pack: extension for client-side status report
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt           |  14 +
 Documentation/githooks.txt                 |  58 ++
 Makefile                                   |   1 +
 builtin/receive-pack.c                     | 334 ++++++++-
 connect.c                                  |   3 +-
 connect.h                                  |   1 +
 refs.c                                     |  11 +-
 refs.h                                     |   1 +
 remote.c                                   |  28 +
 remote.h                                   |   3 +
 send-pack.c                                |  13 +-
 t/helper/test-proc-receive.c               | 178 +++++
 t/helper/test-tool.c                       |   1 +
 t/helper/test-tool.h                       |   1 +
 t/t5411-proc-receive-hook.sh               |  75 ++
 t/t5411/common-functions.sh                |  54 ++
 t/t5411/common-test-cases.sh               | 827 +++++++++++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh |  86 +++
 t/t5516-fetch-push.sh                      |   2 +-
 transport-helper.c                         |  66 +-
 transport.c                                |  42 +-
 21 files changed, 1745 insertions(+), 54 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v10 1/8] transport: not report a non-head push as a branch
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-12 20:26             ` Junio C Hamano
  2020-04-12 13:30           ` [PATCH v10 2/8] receive-pack: add new proc-receive hook Jiang Xin
                             ` (6 subsequent siblings)
  8 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh               | 75 +++++++++++++++++++
 t/t5411/common-functions.sh                | 50 +++++++++++++
 t/t5411/common-test-cases.sh               | 43 +++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh | 86 ++++++++++++++++++++++
 t/t5516-fetch-push.sh                      |  2 +-
 transport.c                                |  9 ++-
 6 files changed, 261 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..ef289fe92a
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace the URL of different protocol for the upstream repository
+# with a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common | sed \
+		-e "s#To ../upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream.git/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream.git/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream.git/hooks/pre-receive \
+		upstream.git/hooks/post-receive &&
+
+	upstream=upstream.git
+'
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6e400c0625
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,50 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.
+make_user_friendly_and_stable_output_common () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
+}
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
new file mode 100644
index 0000000000..23655846e4
--- /dev/null
+++ b/t/t5411/common-test-cases.sh
@@ -0,0 +1,43 @@
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
new file mode 100755
index 0000000000..e2446d4d32
--- /dev/null
+++ b/t/t5412-proc-receive-hook-http-protocol.sh
@@ -0,0 +1,86 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook for HTTP protocol'
+
+. ./test-lib.sh
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace the URL of different protocol for the upstream repository
+# with a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common | sed \
+		-e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git -C upstream.git config http.receivepack true &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	cat >upstream.git/hooks/pre-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# pre-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	cat >upstream.git/hooks/post-receive <<-\EOF &&
+	#!/bin/sh
+
+	echo >&2 "# post-receive hook"
+
+	while read old new ref
+	do
+		echo >&2 "post-receive< $old $new $ref"
+	done
+	EOF
+
+	chmod a+x \
+		upstream.git/hooks/pre-receive \
+		upstream.git/hooks/post-receive &&
+
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..272c0f4046 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 2/8] receive-pack: add new proc-receive hook
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 1/8] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-12 21:30             ` Junio C Hamano
  2020-04-12 13:30           ` [PATCH v10 3/8] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                             ` (5 subsequent siblings)
  8 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
    # NO, I reject it.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status are given in key=value pairs after the null
    # character.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
                forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in key-value pairs as extended status of the report
line.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 260 +++++++++++++++-
 t/helper/test-proc-receive.c | 168 ++++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411/common-functions.sh  |  10 +-
 t/t5411/common-test-cases.sh | 576 +++++++++++++++++++++++++++++++++++
 7 files changed, 1011 insertions(+), 6 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..1a57c5bb3b 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +820,236 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	int code = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ') {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status) {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+		*status++ = '\0';
+		if (strlen(status) > 2 && *(status + 2) == ' ') {
+			msg = status + 2;
+			*msg++ = '\0';
+		}
+		if (strlen(status) != 2) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
+				    status, reader->line);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on ref of builtin command: %s\n",
+				    refname);
+			continue;
+		}
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				hint->error_string = xstrdup(msg);
+			else
+				hint->error_string = "failed";
+			code = -1;
+		} else if (strcmp("ok", status)) {
+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
+				    status, reader->line);
+			code = -1;
+			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
+			continue;
+		}
+		oidcpy(&hint->old_oid, &old_oid);
+		oidcpy(&hint->new_oid, &new_oid);
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->error_string &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "no report from proc-receive";
+		    code = -1;
+		}
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1625,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1665,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1701,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1731,20 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/* Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1771,12 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+				cmd->error_string = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..8fb143d692
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,168 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 6e400c0625..ef3913bbb5 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -34,8 +34,11 @@ create_commits_in () {
 # Format the output of git-push, git-show-ref and other commands to make a
 # user-friendly and stable text.  We can easily prepare the expect text
 # without having to worry about future changes of the commit ID and spaces
-# of the output.  We also replce single quotes with double quotes, because
-# it is boring to prepare unquoted single quotes in expect txt.
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect txt.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
 make_user_friendly_and_stable_output_common () {
 	sed \
 		-e "s/  *\$//" \
@@ -46,5 +49,6 @@ make_user_friendly_and_stable_output_common () {
 		-e "s/$TAG/<TAG-v123>/g" \
 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
-		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "/^error: / d"
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 23655846e4..8e30c8f31e 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -41,3 +41,579 @@ test_expect_success "normal git-push command" '
 	EOF
 	test_cmp expect actual
 '
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "no proc-receive hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad version)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v --version 2
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: unknown version" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { s/   */ /g; p; }" \
+		-e "/^ / { s/   */ /g; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	sed -n -e "/^remote: error:/ { s/   */ /g; p; }" \
+		<actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive bad protocol: no report" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad oid" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: no status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: unknown status" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (bad status)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic bad status"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive bad protocol: bad status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
+	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (no message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (has message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report status on builtin command)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: warning on report for builtin command" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: no report from proc-receive" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 3/8] refs.c: refactor to reuse ref_is_hidden()
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
                             ` (2 preceding siblings ...)
  2020-04-12 13:30           ` [PATCH v10 2/8] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-12 21:38             ` Junio C Hamano
  2020-04-12 13:30           ` [PATCH v10 4/8] receive-pack: new config receive.procReceiveRefs Jiang Xin
                             ` (4 subsequent siblings)
  8 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 11 ++++++++---
 refs.h |  1 +
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..229159ea1a 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,13 +1389,18 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 }
 
 int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_is_matched(hide_refs, refname, refname_full);
+}
+
+int ref_is_matched(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
diff --git a/refs.h b/refs.h
index 545029c6d8..a2ea043f7f 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_is_matched(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 4/8] receive-pack: new config receive.procReceiveRefs
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
                             ` (3 preceding siblings ...)
  2020-04-12 13:30           ` [PATCH v10 3/8] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-12 21:46             ` Junio C Hamano
  2020-04-12 13:30           ` [PATCH v10 5/8] connect: export parse_feature_value() Jiang Xin
                             ` (3 subsequent siblings)
  8 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings match the reference names of the commands
which are sent from git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt |  14 ++++
 builtin/receive-pack.c           |  43 ++++++++--
 t/t5411/common-test-cases.sh     | 130 +++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..e7b967feeb 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running the hook "proc-receive".
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 1a57c5bb3b..bdd0f0f448 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_insert(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1734,15 +1749,26 @@ static void execute_commands(struct command *commands,
 	/* Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2196,6 +2222,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2311,5 +2339,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 8e30c8f31e..d6ad0e1bc8 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -53,6 +53,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -617,3 +625,125 @@ test_expect_success "push with options" '
 	EOF
 	test_cmp expect actual
 '
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$B $A refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "report update of all special refs" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/review/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$A $B refs/for/master/topic ok"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "report mixed refs update" '
+	git -C workbench push origin \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 5/8] connect: export parse_feature_value()
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
                             ` (4 preceding siblings ...)
  2020-04-12 13:30           ` [PATCH v10 4/8] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 6/8] receive-pack: extension for server-side report Jiang Xin
                             ` (2 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Export `parse_feature_value()` to parse extended status of report for
"proc-receive" and "receive-pack".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 connect.c | 3 +--
 connect.h | 1 +
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/connect.c b/connect.c
index b6451ab5e8..08e0616726 100644
--- a/connect.c
+++ b/connect.c
@@ -18,7 +18,6 @@
 
 static char *server_capabilities_v1;
 static struct argv_array server_capabilities_v2 = ARGV_ARRAY_INIT;
-static const char *parse_feature_value(const char *, const char *, int *);
 
 static int check_ref(const char *name, unsigned int flags)
 {
@@ -447,7 +446,7 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
 	return list;
 }
 
-static const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
+const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
 {
 	int len;
 
diff --git a/connect.h b/connect.h
index 5f2382e018..17b3252cbc 100644
--- a/connect.h
+++ b/connect.h
@@ -12,6 +12,7 @@ int finish_connect(struct child_process *conn);
 int git_connection_is_socket(struct child_process *conn);
 int server_supports(const char *feature);
 int parse_feature_request(const char *features, const char *feature);
+const char *parse_feature_value(const char *feature_list, const char *feature, int *len_ret);
 const char *server_feature_value(const char *feature, int *len_ret);
 int url_is_local_not_ssh(const char *url);
 
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 6/8] receive-pack: extension for server-side report
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
                             ` (5 preceding siblings ...)
  2020-04-12 13:30           ` [PATCH v10 5/8] connect: export parse_feature_value() Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 7/8] send-pack: extension for client-side status report Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 8/8] doc: add documentation for the proc-receive hook Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Each command for the "proc-receive" hook may point to a pseudo-reference
and always has a zero-old as its old-oid.  But the "proc-receive" hook
may update an alternate reference and the reference may exist already
(has a non-zero old-oid).  And we may want certain commands to bypass
the "proc-receive" hook and let "receive-pack" handle them.

In order to report proper reference name, old-oid, and let certain
commands fall through, an extensible reporting method for "proc-receive"
and "receive-pack" is introduced.

The "proc-receive" hook can report its results in the following format:

    # OK, run this command successfully.
    PKT-LINE(<old-oid> <new-oid> <ref> ok)

    # NO, I reject it.
    PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)

    # Fall through, let "receive-pack" to execute it.
    PKT-LINE(<old-oid> <new-oid> <ref> ft)

    # OK, but has an alternate reference.  The alternate reference name
    # and other status are given in key=value pairs after the null
    # character.
    PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
             forced-update)

The reporting function for "receive-pack" is also extended using a
backward compatible way by adding key-value pairs after an null
character, like:

    # OK, run this command successfully with optional extended-status.
    ok <reference>\0ref=refs/pull/123/head old-oid=...

    # NO, I reject it.
    ng <reference> <error message>

This commit only includes changes on the server side, so we can make
a compatible test on old version of a Git client.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c       | 51 +++++++++++++++++----
 t/helper/test-proc-receive.c | 14 +++++-
 t/t5411/common-test-cases.sh | 88 ++++++++++++++++++++++++++++++++++--
 3 files changed, 137 insertions(+), 16 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index bdd0f0f448..9f5309a014 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -328,6 +328,7 @@ static void write_head_info(void)
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extended_status;
 	unsigned int skip_update:1,
 		     did_not_exist:1,
 		     run_proc_receive:2;
@@ -850,6 +851,7 @@ static int read_proc_receive_report(struct packet_reader *reader,
 {
 	struct command *cmd;
 	struct command *hint = NULL;
+	struct strbuf extended_status = STRBUF_INIT;
 	int code = 0;
 
 	for (;;) {
@@ -858,9 +860,11 @@ static int read_proc_receive_report(struct packet_reader *reader,
 		const char *p;
 		char *status;
 		char *msg = NULL;
+		int len;
 
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
+		len = strlen(reader->line);
 		if (parse_oid_hex(reader->line, &old_oid, &p) ||
 		    *p++ != ' ' ||
 		    parse_oid_hex(p, &new_oid, &p) ||
@@ -912,6 +916,9 @@ static int read_proc_receive_report(struct packet_reader *reader,
 			else
 				hint->error_string = "failed";
 			code = -1;
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
 		} else if (strcmp("ok", status)) {
 			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
 				    status, reader->line);
@@ -919,9 +926,24 @@ static int read_proc_receive_report(struct packet_reader *reader,
 			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
 			continue;
 		}
-		oidcpy(&hint->old_oid, &old_oid);
-		oidcpy(&hint->new_oid, &new_oid);
-		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		if (reader->pktlen > len)
+			strbuf_addstr(&extended_status, (char *)reader->line + len + 1);
+		if (oidcmp(&hint->old_oid, &old_oid)) {
+			oidcpy(&hint->old_oid, &old_oid);
+			strbuf_addf(&extended_status, "%sold-oid=%s",
+				    extended_status.len > 0 ? " ": "",
+				    oid_to_hex(&old_oid));
+		}
+		if (oidcmp(&hint->new_oid, &new_oid)) {
+			oidcpy(&hint->new_oid, &new_oid);
+			strbuf_addf(&extended_status, "%snew-oid=%s",
+				    extended_status.len > 0 ? " ": "",
+				    oid_to_hex(&new_oid));
+		}
+		if (extended_status.len > 0)
+			hint->extended_status = strbuf_detach(&extended_status, NULL);
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next)
@@ -2179,12 +2201,23 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
-			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+		if (!cmd->error_string) {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, '\0',
+						 cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+		} else {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ng %s %s%c%s\n",
+						 cmd->ref_name, cmd->error_string,
+						 '\0', cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ng %s %s\n",
+						 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index 8fb143d692..c9f5c5aae4 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -159,8 +159,18 @@ int cmd__proc_receive(int argc, const char **argv)
 	}
 
 	if (returns.nr)
-		for_each_string_list_item(item, &returns)
-			packet_write_fmt(1, "%s\n", item->string);
+		for_each_string_list_item(item, &returns) {
+			char *p;
+
+			p = strstr(item->string, "\\0");
+			if (p) {
+				*p = '\0';
+				p += 2;
+				packet_write_fmt(1, "%s%c%s\n", item->string, '\0', p);
+			} else {
+				packet_write_fmt(1, "%s\n", item->string);
+			}
+		}
 	packet_flush(1);
 	sigchain_pop(SIGPIPE);
 
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index d6ad0e1bc8..35fe73c34e 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -640,8 +640,8 @@ test_expect_success "setup proc-receive hook" '
 
 	test-tool proc-receive -v \
 		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
-		-r "$ZERO_OID $A refs/for/next/topic ok" \
-		-r "$B $A refs/for/master/topic ok"
+		-r "$ZERO_OID $A refs/for/next/topic ok\0ref=refs/pull/123/head" \
+		-r "$B $A refs/for/master/topic ok\0ref=refs/pull/124/head forced-update"
 	EOF
 	chmod a+x "$upstream/hooks/proc-receive"
 '
@@ -666,8 +666,8 @@ test_expect_success "report update of all special refs" '
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
-	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok\0ref=refs/pull/124/head forced-update
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
@@ -701,9 +701,10 @@ test_expect_success "setup proc-receive hook" '
 
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
-# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
 test_expect_success "report mixed refs update" '
 	git -C workbench push origin \
+		$B:refs/heads/master \
 		HEAD:refs/heads/bar \
 		HEAD:refs/heads/baz \
 		HEAD:refs/for/next/topic \
@@ -713,6 +714,7 @@ test_expect_success "report mixed refs update" '
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
@@ -724,12 +726,14 @@ test_expect_success "report mixed refs update" '
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
 	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok
 	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
@@ -743,6 +747,80 @@ test_expect_success "report mixed refs update" '
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "config receive.procReceiveRefs for all refs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+test_expect_success "setup proc-receive hook" '
+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
+	#!/bin/sh
+
+	printf >&2 "# proc-receive hook\n"
+
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok\0ref=refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok\0ref=refs/pull/124/head forced-update"
+	EOF
+	chmod a+x "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "report test: fallthrough" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok\0ref=refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/124/head forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/for/next/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/master
 	EOF
 	test_cmp expect actual
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 7/8] send-pack: extension for client-side status report
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
                             ` (6 preceding siblings ...)
  2020-04-12 13:30           ` [PATCH v10 6/8] receive-pack: extension for server-side report Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  2020-04-12 13:30           ` [PATCH v10 8/8] doc: add documentation for the proc-receive hook Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This is the client-side extension for status report of git-push.

The "proc-receive" hook may receive a command for a pseudo-reference
with a zero-old as its old-oid, while the result of the hook may point
to an alternate reference and the reference may exist already with a
non-zero old-oid.  Git client may receive a report with extened status
as the following example:

    ok refs/for/master/topic\0ref=refs/pull/123/head old-oid=...

Parse the key-value pairs in the extended status after the null
character of the report, and show the proper reference name, old-oid
and new-oid to the user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 remote.c                     | 28 +++++++++++++++
 remote.h                     |  3 ++
 send-pack.c                  | 13 ++++++-
 t/t5411/common-test-cases.sh | 10 +++---
 transport-helper.c           | 66 +++++++++++++++++++-----------------
 transport.c                  | 33 +++++++++++++++---
 6 files changed, 112 insertions(+), 41 deletions(-)

diff --git a/remote.c b/remote.c
index c43196ec06..0b22d9e14c 100644
--- a/remote.c
+++ b/remote.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "config.h"
+#include "connect.h"
 #include "remote.h"
 #include "refs.h"
 #include "refspec.h"
@@ -2311,3 +2312,30 @@ void apply_push_cas(struct push_cas_option *cas,
 	for (ref = remote_refs; ref; ref = ref->next)
 		apply_cas(cas, remote, ref);
 }
+
+void update_ref_from_remote_status(struct ref *ref)
+{
+	char *val;
+	char c;
+	int len;
+
+	if (!ref->remote_status)
+		return;
+
+	val = (char *)parse_feature_value(ref->remote_status, "old-oid", &len);
+	if (val && len) {
+		c = *(val+len);
+		*(val+len) = '\0';
+		get_oid_hex(val, &ref->old_oid);
+		*(val+len) = c;
+	}
+	val = (char *)parse_feature_value(ref->remote_status, "new-oid", &len);
+	if (val && len) {
+		c = *(val+len);
+		*(val+len) = '\0';
+		get_oid_hex(val, &ref->new_oid);
+		*(val+len) = c;
+	}
+	if (parse_feature_request(ref->remote_status, "forced-update"))
+		ref->forced_update = 1;
+}
diff --git a/remote.h b/remote.h
index 11d8719b58..09cf7014e3 100644
--- a/remote.h
+++ b/remote.h
@@ -345,4 +345,7 @@ int parseopt_push_cas_option(const struct option *, const char *arg, int unset);
 int is_empty_cas(const struct push_cas_option *);
 void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
 
+/* Parse key-value pairs of remote_status and update the reference accordingly */
+void update_ref_from_remote_status(struct ref *ref);
+
 #endif
diff --git a/send-pack.c b/send-pack.c
index 0407841ae8..ab294c9426 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -159,6 +159,9 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 	while (1) {
 		const char *refname;
 		char *msg;
+		char *extended_status = NULL;
+		int len;
+
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
@@ -167,10 +170,13 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			break;
 		}
 
+		len = strlen(reader->line);
 		refname = reader->line + 3;
 		msg = strchr(refname, ' ');
 		if (msg)
 			*msg++ = '\0';
+		if (reader->pktlen > len)
+			extended_status = (char *)reader->line + len + 1;
 
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
@@ -194,7 +200,12 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			hint->status = REF_STATUS_REMOTE_REJECT;
 			ret = -1;
 		}
-		hint->remote_status = xstrdup_or_null(msg);
+		if (msg) {
+			hint->remote_status = xstrdup(msg);
+		} else if (extended_status) {
+			hint->remote_status = xstrdup(extended_status);
+			update_ref_from_remote_status(hint);
+		}
 		/* start our next search from the next ref */
 		hint = hint->next;
 	}
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 35fe73c34e..413e6502be 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -673,9 +673,9 @@ test_expect_success "report update of all special refs" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/review/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -738,7 +738,7 @@ test_expect_success "report mixed refs update" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -812,8 +812,8 @@ test_expect_success "report test: fallthrough" '
 	 <OID-A>..<OID-B> <COMMIT-B> -> bar
 	 - [deleted] foo
 	 + <OID-B>...<OID-A> HEAD -> master (forced update)
-	 * [new reference] HEAD -> refs/for/master/topic
-	 * [new reference] HEAD -> refs/for/next/topic
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..d2179631cf 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
@@ -802,6 +804,8 @@ static int push_update_ref_status(struct strbuf *buf,
 	(*ref)->status = status;
 	(*ref)->forced_update |= forced;
 	(*ref)->remote_status = msg;
+	if (status == REF_STATUS_OK && (*ref)->remote_status)
+		update_ref_from_remote_status(*ref);
 	return !(status == REF_STATUS_OK);
 }
 
diff --git a/transport.c b/transport.c
index 272c0f4046..b0451c7a3c 100644
--- a/transport.c
+++ b/transport.c
@@ -463,11 +463,30 @@ static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	char *from_name = NULL;
+	char *to_name = NULL;
+	const char *val;
+	int len;
+
+	if (from && from->remote_status) {
+		val = parse_feature_value(from->remote_status, "ref", &len);
+		if (val && len)
+			 from_name = xmemdupz(val, len);
+	}
+	if (to && to->remote_status) {
+		val = parse_feature_value(to->remote_status, "ref", &len);
+		if (val && len)
+			 to_name = xmemdupz(val, len);
+	}
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag,
+				from_name ? from_name : from->name,
+				to_name ? to_name: to->name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag,
+				to_name ? to_name: to->name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +500,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from_name ? from_name : from->name),
+				prettify_refname(to_name ? to_name : to->name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name ? to_name : to->name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -491,6 +512,10 @@ static void print_ref_status(char flag, const char *summary,
 		}
 		fputc('\n', stderr);
 	}
+	if (from_name)
+		free(from_name);
+	if (to_name)
+		free(to_name);
 }
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v10 8/8] doc: add documentation for the proc-receive hook
  2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
                             ` (7 preceding siblings ...)
  2020-04-12 13:30           ` [PATCH v10 7/8] send-pack: extension for client-side status report Jiang Xin
@ 2020-04-12 13:30           ` Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-12 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 58 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..1c528e2fc4 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,64 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
+    # NO, I reject it.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status are given in key=value pairs after the null
+    # character.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
+                forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook may return
+different OID and different reference name as extended status of the
+report line.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 1/8] transport: not report a non-head push as a branch
  2020-04-12 13:30           ` [PATCH v10 1/8] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-12 20:26             ` Junio C Hamano
  2020-04-13 11:15               ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-04-12 20:26 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

>  create mode 100755 t/t5411-proc-receive-hook.sh
>  create mode 100644 t/t5411/common-functions.sh
>  create mode 100644 t/t5411/common-test-cases.sh
>  create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh
>
> diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
> new file mode 100755
> index 0000000000..ef289fe92a
> --- /dev/null
> +++ b/t/t5411-proc-receive-hook.sh
> @@ -0,0 +1,75 @@
> +#!/bin/sh
> +#
> +# Copyright (c) 2020 Jiang Xin
> +#
> +
> +test_description='Test proc-receive hook'
> +
> +. ./test-lib.sh
> +
> +. "$TEST_DIRECTORY"/t5411/common-functions.sh
> +
> +# Format the output of git-push, git-show-ref and other commands to make a
> +# user-friendly and stable text.  In addition to the common format method,
> +# we also replace the URL of different protocol for the upstream repository
> +# with a fixed pattern.
> +make_user_friendly_and_stable_output () {
> +	make_user_friendly_and_stable_output_common | sed \
> +		-e "s#To ../upstream.git#To <URL/of/upstream.git>#"

Break the line immediately after the pipe, and begin the next line
with the command.  That way, the shell knows that you haven't
finished giving the command at the end of the first line, and you do
not have to use a backslash, i.e.

    make_user_friendly |
    sed -e 's/from/to/'

> +		cd workbench &&
> +		# Try to make a stable fixed width for abbreviated commit ID,
> +		# this fixed-width oid will be replaced with "<OID>".
> +		git config core.abbrev 7 &&
> +		git remote add origin ../upstream.git &&
> +		git update-ref refs/heads/master $A &&
> +		git tag -m "v123" v123 $A &&
> +		git push origin \
> +			$B:refs/heads/master \
> +			$A:refs/heads/next
> +	) &&
> +	TAG=$(git -C workbench rev-parse v123) &&
> +
> +	# setup pre-receive hook
> +	cat >upstream.git/hooks/pre-receive <<-\EOF &&

Use write_script and you don't need the chmod at the end.

> +	#!/bin/sh
> +
> +	echo >&2 "# pre-receive hook"
> +
> +	while read old new ref
> +	do
> +		echo >&2 "pre-receive< $old $new $ref"
> +	done
> +	EOF

Perhaps "exec >&2" upfront?

> +	# setup post-receive hook
> +	cat >upstream.git/hooks/post-receive <<-\EOF &&
> +	#!/bin/sh
> +
> +	echo >&2 "# post-receive hook"
> +
> +	while read old new ref
> +	do
> +		echo >&2 "post-receive< $old $new $ref"
> +	done
> +	EOF
> +
> +	chmod a+x \
> +		upstream.git/hooks/pre-receive \
> +		upstream.git/hooks/post-receive &&

Ditto.

> +	upstream=upstream.git
> +'
> +
> +# Include test cases for both file and HTTP protocol
> +. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
> +
> +test_done


> diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
> new file mode 100644
> index 0000000000..6e400c0625
> --- /dev/null
> +++ b/t/t5411/common-functions.sh
> @@ -0,0 +1,50 @@

It is good that this omits "#!" and leaves the file without +x bit.

> +# Format the output of git-push, git-show-ref and other commands to make a
> +# user-friendly and stable text.  We can easily prepare the expect text
> +# without having to worry about future changes of the commit ID and spaces
> +# of the output.  We also replce single quotes with double quotes, because
> +# it is boring to prepare unquoted single quotes in expect txt.

s/txt/text/

> +make_user_friendly_and_stable_output_common () {
> +	sed \
> +		-e "s/  *\$//" \
> +		-e "s/   */ /g" \
> +		-e "s/'/\"/g" \
> +		-e "s/$A/<COMMIT-A>/g" \
> +		-e "s/$B/<COMMIT-B>/g" \
> +		-e "s/$TAG/<TAG-v123>/g" \
> +		-e "s/$ZERO_OID/<ZERO-OID>/g" \
> +		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
> +		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
> +}

So a full object name for commit "A" becomes <COMMIT-A> while an
abbreviated one becomes <OID-A>?  OK.  I wonder if we force the full
length (i.e. no abbreviation), the tests become simpler, but it is
OK.  As long as A and B does not share the same 7 hexdigits in any
future hash function we choose, the above will do the right thing
;-)

> diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
> new file mode 100755
> index 0000000000..e2446d4d32
> --- /dev/null
> +++ b/t/t5412-proc-receive-hook-http-protocol.sh
> @@ -0,0 +1,86 @@
> +#!/bin/sh
> +#
> +# Copyright (c) 2020 Jiang Xin
> +#
> +
> +test_description='Test proc-receive hook for HTTP protocol'
> +
> +. ./test-lib.sh
> +
> +ROOT_PATH="$PWD"
> +. "$TEST_DIRECTORY"/lib-gpg.sh
> +. "$TEST_DIRECTORY"/lib-httpd.sh
> +. "$TEST_DIRECTORY"/lib-terminal.sh
> +start_httpd
> +
> +. "$TEST_DIRECTORY"/t5411/common-functions.sh
> +
> +# Format the output of git-push, git-show-ref and other commands to make a
> +# user-friendly and stable text.  In addition to the common format method,
> +# we also replace the URL of different protocol for the upstream repository
> +# with a fixed pattern.
> +make_user_friendly_and_stable_output () {
> +	make_user_friendly_and_stable_output_common | sed \
> +		-e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
> +}

Ditto.

> +# Refs of upstream : master(B)  next(A)
> +# Refs of workbench: master(A)           tags/v123
> +test_expect_success "setup" '
> +	git init --bare upstream.git &&
> +	git -C upstream.git config http.receivepack true &&
> +	git init workbench &&
> +	create_commits_in workbench A B &&
> +	(
> +		cd workbench &&
> +		# Try to make a stable fixed width for abbreviated commit ID,
> +		# this fixed-width oid will be replaced with "<OID>".
> +		git config core.abbrev 7 &&
> +		git remote add origin ../upstream.git &&
> +		git update-ref refs/heads/master $A &&
> +		git tag -m "v123" v123 $A &&
> +		git push origin \
> +			$B:refs/heads/master \
> +			$A:refs/heads/next
> +	) &&
> +	TAG=$(git -C workbench rev-parse v123) &&
> +
> +	# setup pre-receive hook
> +	cat >upstream.git/hooks/pre-receive <<-\EOF &&
> +	#!/bin/sh

Ditto.

> +
> +	echo >&2 "# pre-receive hook"
> +
> +	while read old new ref
> +	do
> +		echo >&2 "pre-receive< $old $new $ref"
> +	done
> +	EOF
> +
> +	# setup post-receive hook
> +	cat >upstream.git/hooks/post-receive <<-\EOF &&
> +	#!/bin/sh
> +
> +	echo >&2 "# post-receive hook"
> +
> +	while read old new ref
> +	do
> +		echo >&2 "post-receive< $old $new $ref"
> +	done
> +	EOF
> +
> +	chmod a+x \
> +		upstream.git/hooks/pre-receive \
> +		upstream.git/hooks/post-receive &&
> +
> +	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
> +	mv upstream.git "$upstream" &&
> +	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
> +'
> +
> +setup_askpass_helper
> +
> +# Include test cases for both file and HTTP protocol
> +. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
> +
> +test_done
> diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
> index 9ff041a093..9e4b9313b5 100755
> --- a/t/t5516-fetch-push.sh
> +++ b/t/t5516-fetch-push.sh
> @@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
>  test_expect_success 'push --porcelain' '
>  	mk_empty testrepo &&
>  	echo >.git/foo  "To testrepo" &&
> -	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
> +	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
>  	echo >>.git/foo "Done" &&
>  	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
>  	(
> diff --git a/transport.c b/transport.c
> index 1fdc7dac1a..272c0f4046 100644
> --- a/transport.c
> +++ b/transport.c
> @@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
>  				 porcelain, summary_width);
>  	else if (is_null_oid(&ref->old_oid))

This block is about an update to missing "old", i.e. a creation.

>  		print_ref_status('*',
> -			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
> -			"[new branch]"),
> -			ref, ref->peer_ref, NULL, porcelain, summary_width);

We used to say "if outside refs/tags/, let's call it branch".

> +				 (starts_with(ref->name, "refs/tags/")
> +				  ? "[new tag]"
> +				  : (starts_with(ref->name, "refs/heads/")
> +				     ? "[new branch]"
> +				     : "[new reference]")),
> +				 ref, ref->peer_ref, NULL, porcelain, summary_width);

Now we say "outside refs/heads/ and refs/tags/, that's just a
reference, different from branch".  OK.

Do we ever update refs/remotes/ with this codepath?  Would we want
to label it differently, i.e. as "remote-tracking branch"?

>  	else {
>  		struct strbuf quickref = STRBUF_INIT;
>  		char type;

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 2/8] receive-pack: add new proc-receive hook
  2020-04-12 13:30           ` [PATCH v10 2/8] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-12 21:30             ` Junio C Hamano
  2020-04-13 10:58               ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-04-12 21:30 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> The filter will check the prefix of the reference name, and if the
> command has a special reference name, the filter will turn a specific
> field (`run_proc_receive`) on for the command.  Commands with this filed
> turned on will be executed by a new handler (an hook named
> "proc-receive") instead of the internal `execute_commands` function.
> We can use this "proc-receive" command to create pull requests or send
> emails for code review.
>
> Suggested by Junio, this "proc-receive" hook reads the commands,
> push-options (optional), and send result using a protocol in pkt-line
> format.  In the following example, The letter "S" stands for
> "receive-pack" and letter "H" stands for the hook.

Heh, you're giving me too much credit here.  The exchange
illustrated below makes sense to me, ...

>     # Version and features negotiation.
>     S: PKT-LINE(version=1\0push-options atomic...)
>     S: flush-pkt
>     H: PKT-LINE(version=1\0push-options...)
>     H: flush-pkt
>
>     # Send commands from server to the hook.
>     S: PKT-LINE(<old-oid> <new-oid> <ref>)
>     S: ... ...
>     S: flush-pkt
>     # Send push-options only if the 'push-options' feature is enabled.
>     S: PKT-LINE(push-option)
>     S: ... ...
>     S: flush-pkt
>
>     # Receive result from the hook.
>     # OK, run this command successfully.
>     H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
>     # NO, I reject it.
>     H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
>     # Fall through, let 'receive-pack' to execute it.
>     H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
>     # OK, but has an alternate reference.  The alternate reference name
>     # and other status are given in key=value pairs after the null
>     # character.
>     H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
>                 forced-update)

The semantics of this one is fuzzy.  We made an update to a ref that
is different from the one that was requested (presumably that is what
is reported after "\0ref="), OK, but did we update the ref in the
request, too, or did we leave the ref in the request intact?  Or, do
we say "no\0ref=..." if we don't update the requested ref and
instead update a different one?  Let's hold the answer at this point
and keep thinking...

>     H: ... ...
>     H: flush-pkt
>
> After receiving a command, the hook will execute the command, and may
> create/update different reference.  For example, a command for a pseudo
> reference "refs/for/master/topic" may create/update different reference
> such as "refs/pull/123/head".  The alternate reference name and other
> status are given in key-value pairs as extended status of the report
> line.
>
> The list of commands returned from "proc-receive" will replace the
> relevant commands that are sent from user to "receive-pack", and
> "receive-pack" will continue to run the "execute_commands" function and
> other routines.  Finally, the result of the execution of these commands
> will be reported to end user.

It is not clear if there needs to be any correspondence between the
sequence of input commands and the sequence of output commands.  I
am guessing that there is not any and the hook is allowed to do
anything as it wants to.  For example:

 - it is OK to execute them all and report them, but in a totally
   random order.

 - it is OK to ignore all input and report no update (not even
   "ng").

 - it is OK to be asked to update one ref, but update and report
   updates to two refs that are not related to the requested ref.

Is my understanding correct?

Or does the design tie the set of input and output ref updates
closely together, i.e. one input corresponds to one output and they
are in order, so all the hook is allowed to do is

 - to execute and update as it is told, and report "ok",

 - to reject and report "ng", or

 - to update *another* ref without updating the requested one, with
   "ok\0ref=..." mechanism.

I am not sure which one is more sensible, though.

If we choose to use the "anything goes" approach, I do not think
there is no need for the "ok\nref=..." mechanism---we can just give
two output records, one "ok" for the original request, and a new
"ok" that reports the "we updated this one instead".

> +#define RUN_PROC_RECEIVE_SCHEDULED	1
> +#define RUN_PROC_RECEIVE_RETURNED	2
>  struct command {
>  	struct command *next;
>  	const char *error_string;
>  	unsigned int skip_update:1,
> -		     did_not_exist:1;
> +		     did_not_exist:1,
> +		     run_proc_receive:2;
>  	int index;
>  	struct object_id old_oid;
>  	struct object_id new_oid;
> @@ -817,6 +820,236 @@ static int run_update_hook(struct command *cmd)
>  	return finish_command(&proc);
>  }
>  
> +static struct command *find_command_by_refname(const struct command *list,
> +					       const char *refname)
> +{
> +	for (; list; list = list->next)
> +		if (!strcmp(list->ref_name, refname))
> +			return (struct command *)list;
> +	return NULL;
> +}
> +
> +static int read_proc_receive_report(struct packet_reader *reader,
> +				    struct command *commands,
> +				    struct strbuf *errmsg)
> +{
> +	struct command *cmd;
> +	struct command *hint = NULL;
> +	int code = 0;
> +
> +	for (;;) {
> +		struct object_id old_oid, new_oid;
> +		const char *refname;
> +		const char *p;
> +		char *status;
> +		char *msg = NULL;
> +
> +		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
> +			break;
> +		if (parse_oid_hex(reader->line, &old_oid, &p) ||
> +		    *p++ != ' ' ||
> +		    parse_oid_hex(p, &new_oid, &p) ||
> +		    *p++ != ' ') {
> +			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
> +				    reader->line);
> +			code = -1;
> +			continue;
> +		}
> +
> +		refname = p;
> +		status = strchr(p, ' ');
> +		if (!status) {
> +			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
> +				    reader->line);
> +			code = -1;
> +			continue;
> +		}
> +		*status++ = '\0';
> +		if (strlen(status) > 2 && *(status + 2) == ' ') {
> +			msg = status + 2;
> +			*msg++ = '\0';
> +		}

So, here we parsed "<old> <new> <ref> (ok|ng)", with status pointing
at ok/ng, or "<old> <new> <ref> (ok|ng) <why>", with status pointing
at ok/ng and msg pointing at <why>.  refname points at <ref>.

Most notably, "ok\0ref=..." is not handled any specially from a
normal "ok" here.

> +		if (strlen(status) != 2) {
> +			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
> +				    status, reader->line);
> +			code = -1;
> +			continue;
> +		}
> +
> +		/* first try searching at our hint, falling back to all refs */
> +		if (hint)
> +			hint = find_command_by_refname(hint, refname);
> +		if (!hint)
> +			hint = find_command_by_refname(commands, refname);
> +		if (!hint) {
> +			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
> +				    refname);
> +			continue;
> +		}
> +		if (!hint->run_proc_receive) {
> +			strbuf_addf(errmsg, "proc-receive reported status on ref of builtin command: %s\n",
> +				    refname);
> +			continue;

Updates against a request to update certain ref hierarchies by the
hook is rejected here.

> +		}
> +		if (!strcmp(status, "ng")) {
> +			if (msg)
> +				hint->error_string = xstrdup(msg);
> +			else
> +				hint->error_string = "failed";
> +			code = -1;

The <why> in "(ok|ng) <why>" message is only used for "ng", which is
probably good, as the reason why something succeeded is not usually
very interesting.

> +		} else if (strcmp("ok", status)) {
> +			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
> +				    status, reader->line);
> +			code = -1;
> +			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */

But then shouldn't we be complaining if msg is not NULL here,
instead of silently ignoring?  Also we didn't see what happened to
the promised "ok\0ref=..." stuff here, or the passthru "ft".
Puzzled...

> +			continue;
> +		}
> +		oidcpy(&hint->old_oid, &old_oid);
> +		oidcpy(&hint->new_oid, &new_oid);
> +		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;

Or, is this last one the catch all for "ft" (in other words, the
hook does not have to say "ft", but as long as it says two non-SP
letters that are not "ok" nor "ng", it is taken as "ft")?

> +	}
> +
> +	for (cmd = commands; cmd; cmd = cmd->next)
> +		if (cmd->run_proc_receive && !cmd->error_string &&
> +		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
> +		    cmd->error_string = "no report from proc-receive";
> +		    code = -1;
> +		}
> +
> +	return code;
> +}

OK, so this sort-of answers my earlier question.  But not quite...

The output records and the input requests are tied by "<ref>" each
input record wanted to udpate.  The order does not matter, but not
having a corresponding report in the hook's output is like the hook
reporting a "ng" failure.  It also means that the hook can update
two refs in response to one request, but it is awkward.

> +static int run_proc_receive_hook(struct command *commands,
> +				 const struct string_list *push_options)
> +{
> +	struct child_process proc = CHILD_PROCESS_INIT;
> +	struct async muxer;
> +	struct command *cmd;
> +	const char *argv[2];
> +	struct packet_reader reader;
> +	struct strbuf cap = STRBUF_INIT;
> +	struct strbuf errmsg = STRBUF_INIT;
> +	int pr_use_push_options = 0;
> +	int version = 0;
> +	int code;
> +
> +	argv[0] = find_hook("proc-receive");
> +	if (!argv[0]) {
> +		rp_error("cannot find hook 'proc-receive'");
> +		return -1;
> +	}
> +	argv[1] = NULL;
> +
> +	proc.argv = argv;
> +	proc.in = -1;
> +	proc.out = -1;
> +	proc.trace2_hook_name = "proc-receive";
> +

Isn't this a brand new protocol between receive-pack and a hook?  I
am puzzled why we want a choice between using and not using
sideband.  It's not like you are maintaining compatibility with
ancient version of Git that talked with proc-receive hook but
without sideband, because it did not know how to multiplex sideband
communication.  Shouldn't we just always assume that the sideband is
available?

Or are we just letting the hook directly answer the push client and
that is why this thing needs to know if we are using sideband
between us and the client?  I kind of expected that you'd keep the
two communication channels on both sides isolated so that the side
that talks with the hook does not need to know how we are talking
with the client.

> +	if (use_sideband) {
> +		memset(&muxer, 0, sizeof(muxer));
> +		muxer.proc = copy_to_sideband;
> +		muxer.in = -1;
> +		code = start_async(&muxer);
> +		if (code)
> +			return code;
> +		proc.err = muxer.in;
> +	} else {
> +		proc.err = 0;
> +	}
> +
> +	code = start_command(&proc);
> +	if (code) {
> +		if (use_sideband)
> +			finish_async(&muxer);
> +		return code;
> +	}
> +
> +	sigchain_push(SIGPIPE, SIG_IGN);
> +
> +	/* Version negotiaton */
> +	packet_reader_init(&reader, proc.out, NULL, 0,
> +			   PACKET_READ_CHOMP_NEWLINE |
> +			   PACKET_READ_DIE_ON_ERR_PACKET);
> ...
> @@ -1497,6 +1731,20 @@ static void execute_commands(struct command *commands,
>  
>  	reject_updates_to_hidden(commands);
>  
> +	/* Try to find commands that have special prefix in their reference names,

Style?

> +	 * and mark them to run an external "proc-receive" hook later.
> +	 */

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 3/8] refs.c: refactor to reuse ref_is_hidden()
  2020-04-12 13:30           ` [PATCH v10 3/8] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-12 21:38             ` Junio C Hamano
  2020-04-13 11:16               ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-04-12 21:38 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
>
> Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use

ref_is_matched() may not be wrong per-se, but I would imagine that
ref_matches() would sound more natural.

The ancient varilabe hide_refs is probably misnamed and should be
renamed when the codebase is quiescent to hidden_refs but of course
we shouldn't do so before stablizing this series.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 4/8] receive-pack: new config receive.procReceiveRefs
  2020-04-12 13:30           ` [PATCH v10 4/8] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-12 21:46             ` Junio C Hamano
  2020-04-13 11:16               ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-04-12 21:46 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> +	if (strcmp(var, "receive.procreceiverefs") == 0) {
> +		char *prefix;
> +		int len;
> +
> +		if (!value)
> +			return config_error_nonbool(var);
> +		prefix = xstrdup(value);
> +		len = strlen(prefix);
> +		while (len && prefix[len - 1] == '/')
> +			prefix[--len] = '\0';
> +		string_list_insert(&proc_receive_refs, prefix);

This smells like a copy of the hidden-refs configuration parsing,
which uses string_list_append(), and its result is used for look-up
routine you split out in [3/8], so we know it is safe to leave the
string_list that holds the prefix unsorted.  Is there a reason why
we want to use _insert(), which spends extra cycles to make sure
that the string list is sorted, instead (e.g. if we were updating
the look-up routine to take advantage of the ordering of the
string-list, perhaps)?  If so, would it make sense to update the
original to match?  If not, copy more faithfully to the original and
use _append() here, too?

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 2/8] receive-pack: add new proc-receive hook
  2020-04-12 21:30             ` Junio C Hamano
@ 2020-04-13 10:58               ` Jiang Xin
  2020-04-13 21:50                 ` Junio C Hamano
  0 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 10:58 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin


Junio C Hamano <gitster@pobox.com> 于2020年4月13日周一 上午5:30写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
> >     # Receive result from the hook.
> >     # OK, run this command successfully.
> >     H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
> >     # NO, I reject it.
> >     H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
> >     # Fall through, let 'receive-pack' to execute it.
> >     H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
> >     # OK, but has an alternate reference.  The alternate reference name
> >     # and other status are given in key=value pairs after the null
> >     # character.
> >     H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
> >                 forced-update)
>
> The semantics of this one is fuzzy.  We made an update to a ref that
> is different from the one that was requested (presumably that is what
> is reported after "\0ref="), OK, but did we update the ref in the
> request, too, or did we leave the ref in the request intact?  Or, do
> we say "no\0ref=..." if we don't update the requested ref and
> instead update a different one?  Let's hold the answer at this point
> and keep thinking...

Before making a decision, we must consider some limitations or backward
compatibility issues.  See the limitations from following code snippet
of "send-pack.c" and "transport.c":

* "receive-pack" should not send status other than "ok" and "ng" to "send-pack".
  We must keep this for backward compatibility.

    # File: send-pack.c
    #
    152 static int receive_status(struct packet_reader *reader, struct ref *refs)
    153 {
    159     while (1) {
    160         const char *refname;
    161         char *msg;
    162         if (packet_reader_read(reader) != PACKET_READ_NORMAL)
    163             break;
    164         if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
    165             error("invalid ref status from remote: %s", reader->line);
    166             ret = -1;
    167             break;
    168         }

* Do not report unknown reference to "send-pack". Each returned reference
  must be included in `remote_refs`.

    175         /* first try searching at our hint, falling back to all refs */
    176         if (hint)
    177             hint = find_ref_by_name(hint, refname);
    178         if (!hint)
    179             hint = find_ref_by_name(refs, refname);
    180         if (!hint) {
    181             warning("remote reported status on unknown ref: %s",
    182                     refname);

* Do not report unexpected reference to "send-pack".

    185         if (hint->status != REF_STATUS_EXPECTING_REPORT) {
    186             warning("remote reported status on unexpected ref: %s",
    187                     refname);
    188             continue;
    189         }

* Client will complain if a expecting command not is not received from
  the report.

    # File: transport.c
    #
    531 static int print_one_push_status(struct ref *ref, const char *dest, int count,
    532                                  int porcelain, int summary_width)
    540     switch(ref->status) {
    584     case REF_STATUS_EXPECTING_REPORT:
    585         print_ref_status('!', "[remote failure]", ref,
    586                          ref->deletion ? NULL : ref->peer_ref,
    587                          "remote failed to report status",
    588                          porcelain, summary_width);


So we should not invent new status between "receive-pack" and "send-pack".
It is reasonable to to extend the report function from 'receive-pack'
to 'send-pack' (not the report from 'proc-receive' to 'receive-pack')
by adding a null character and multiple key-value pairs as extended
status after the "ok" and "ng" message.

For example, the following git-push command:

    git push origin HEAD:refs/for/master/topic

The refspec points to a pseudo reference "refs/for/master/topic", which
does not exist on the server, so the old-oid of the command to
"receive-pack" must be a zero-oid. The command for 'proc-receive' is:

    <zero-oid> <new-oid> refs/for/master/topic

If "proc-receive" find this `git-push` command is associated with an already
exist pull request (such as "refs/pull/123/head"), it will update this
reference instead of creating a new one.

For this case, we can extend the report line (from "receive-pack" to
"send-pack”) like this:

    ok refs/for/master/topic\0ref=refs/pull/123/head old-oid=<hash> [forced-push]

This extended report line is backward compatible. Old version of git
client will show message like this:

    To upstream.git
     * [new branch]          HEAD -> refs/for/master/topic

New version of git has the knowledge of how to handle the extended-status
after the null character, will print the following message:

    To upstream.git
     + 1234567...7654321     HEAD -> refs/pull/124/head (forced update)


Actually, both 'ok' and 'ng' status have the knowledge of '<msg>', even
though the '<msg>' is ignored by the 'ok' status. After the extension,
both of them can take key-value pairs as extended-status, like:

    ok <reference> [msg]\0key=value ...
    ng <reference> [msg]\0key=value ...

We can use '<msg>' and the extended status to fit our needs in the future.

As for the design of status report from 'proc-receive' to 'receive-pack',
we should follow the same rules:

1. Never report status for non-exists commands.

2. Do not forget to report some of the commands.

3. After receive report for commands from 'proc-receive', we should have
   enough information to generate report to 'send-pack' from these commands.

For the above example, when user push to 'refs/for/master/topic',
'proc-receive' may report to 'receive-pack' like this:

    PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head forced-update)

'send-pack' will save the key-value pairs into the cooresponding `ref->remote_status`.
If the report line has a different '<old-oid>' or '<new-oid>' for the '<ref>',
'send-pack' will substitude the oids of the ref, and append new key-value
pairs (old-oid=<hash> and new-oid=<hash>) to `ref->remote_status`.

> >     H: ... ...
> >     H: flush-pkt
> >
> > After receiving a command, the hook will execute the command, and may
> > create/update different reference.  For example, a command for a pseudo
> > reference "refs/for/master/topic" may create/update different reference
> > such as "refs/pull/123/head".  The alternate reference name and other
> > status are given in key-value pairs as extended status of the report
> > line.
> >
> > The list of commands returned from "proc-receive" will replace the
> > relevant commands that are sent from user to "receive-pack", and
> > "receive-pack" will continue to run the "execute_commands" function and
> > other routines.  Finally, the result of the execution of these commands
> > will be reported to end user.
>
> It is not clear if there needs to be any correspondence between the
> sequence of input commands and the sequence of output commands.  I
> am guessing that there is not any and the hook is allowed to do
> anything as it wants to.  For example:
>
>  - it is OK to execute them all and report them, but in a totally
>    random order.
>
>  - it is OK to ignore all input and report no update (not even
>    "ng").
>
>  - it is OK to be asked to update one ref, but update and report
>    updates to two refs that are not related to the requested ref.
>
> Is my understanding correct?
>
> Or does the design tie the set of input and output ref updates
> closely together, i.e. one input corresponds to one output and they
> are in order, so all the hook is allowed to do is
>
>  - to execute and update as it is told, and report "ok",
>
>  - to reject and report "ng", or
>
>  - to update *another* ref without updating the requested one, with
>    "ok\0ref=..." mechanism.
>
> I am not sure which one is more sensible, though.
>
> If we choose to use the "anything goes" approach, I do not think
> there is no need for the "ok\nref=..." mechanism---we can just give
> two output records, one "ok" for the original request, and a new
> "ok" that reports the "we updated this one instead".

I saw you have noticed the code for replacing the commands of 'receive-pack'
by comparing ref name.

> > +             } else if (strcmp("ok", status)) {
> > +                     strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
> > +                                 status, reader->line);
> > +                     code = -1;
> > +                     /* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
>
> But then shouldn't we be complaining if msg is not NULL here,
> instead of silently ignoring?  Also we didn't see what happened to
> the promised "ok\0ref=..." stuff here, or the passthru "ft".
> Puzzled...

I implement "ft" and "\0key=value..." features in patch 6/8, and it
depends another refactoring commit.  I can squash it to this commit
in next reroll, and rearrange the refactoring commmit and test cases.


> > +                     continue;
> > +             }
> > +             oidcpy(&hint->old_oid, &old_oid);
> > +             oidcpy(&hint->new_oid, &new_oid);
> > +             hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
>
> Or, is this last one the catch all for "ft" (in other words, the
> hook does not have to say "ft", but as long as it says two non-SP
> letters that are not "ok" nor "ng", it is taken as "ft")?

"ft" is implemented in patch 6/8.  This code is used to help to find
missing reported references.

>
> > +     }
> > +
> > +     for (cmd = commands; cmd; cmd = cmd->next)
> > +             if (cmd->run_proc_receive && !cmd->error_string &&
> > +                 !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
> > +                 cmd->error_string = "no report from proc-receive";
> > +                 code = -1;
> > +             }
> > +
> > +     return code;
> > +}
>
> OK, so this sort-of answers my earlier question.  But not quite...
>
> The output records and the input requests are tied by "<ref>" each
> input record wanted to udpate.  The order does not matter, but not
> having a corresponding report in the hook's output is like the hook
> reporting a "ng" failure.  It also means that the hook can update
> two refs in response to one request, but it is awkward.
>

Right sort order of the output commnands has better performace.
The hook can only update commands marked with 'run_proc_receive'.

"update two refs", do you mean duplicate report line from 'proc-receive'? 
If 'proc-receive' really want to update several references for one
command, it must report all of the updates in one report line by adding
message, or adding additional key-value pairs.

> > +static int run_proc_receive_hook(struct command *commands,
> > +                              const struct string_list *push_options)
> > +{
> > +     struct child_process proc = CHILD_PROCESS_INIT;
> > +     struct async muxer;
> > +     struct command *cmd;
> > +     const char *argv[2];
> > +     struct packet_reader reader;
> > +     struct strbuf cap = STRBUF_INIT;
> > +     struct strbuf errmsg = STRBUF_INIT;
> > +     int pr_use_push_options = 0;
> > +     int version = 0;
> > +     int code;
> > +
> > +     argv[0] = find_hook("proc-receive");
> > +     if (!argv[0]) {
> > +             rp_error("cannot find hook 'proc-receive'");
> > +             return -1;
> > +     }
> > +     argv[1] = NULL;
> > +
> > +     proc.argv = argv;
> > +     proc.in = -1;
> > +     proc.out = -1;
> > +     proc.trace2_hook_name = "proc-receive";
> > +
>
> Isn't this a brand new protocol between receive-pack and a hook?  I
> am puzzled why we want a choice between using and not using
> sideband.  It's not like you are maintaining compatibility with
> ancient version of Git that talked with proc-receive hook but
> without sideband, because it did not know how to multiplex sideband
> communication.  Shouldn't we just always assume that the sideband is
> available?

The sideband message stream (stderr of 'proc-receive') will be sent
directly to the client.  If the client is a very old version of git,
is is still safe?

> Or are we just letting the hook directly answer the push client and
> that is why this thing needs to know if we are using sideband
> between us and the client?  I kind of expected that you'd keep the
> two communication channels on both sides isolated so that the side
> that talks with the hook does not need to know how we are talking
> with the client.

In test cases for 'proc-receive', I output some debug info on stderr
of 'proc-receive' hook.  If the client (very old version of git)
does not support sideband, it's ok.

> > +     if (use_sideband) {
> > +             memset(&muxer, 0, sizeof(muxer));
> > +             muxer.proc = copy_to_sideband;
> > +             muxer.in = -1;
> > +             code = start_async(&muxer);
> > +             if (code)
> > +                     return code;
> > +             proc.err = muxer.in;
> > +     } else {
> > +             proc.err = 0;
> > +     }
> > +
> > +     code = start_command(&proc);
> > +     if (code) {
> > +             if (use_sideband)
> > +                     finish_async(&muxer);
> > +             return code;
> > +     }
> > +
> > +     sigchain_push(SIGPIPE, SIG_IGN);
> > +
> > +     /* Version negotiaton */
> > +     packet_reader_init(&reader, proc.out, NULL, 0,
> > +                        PACKET_READ_CHOMP_NEWLINE |
> > +                        PACKET_READ_DIE_ON_ERR_PACKET);
> > ...
> > @@ -1497,6 +1731,20 @@ static void execute_commands(struct command *commands,
> >
> >       reject_updates_to_hidden(commands);
> >
> > +     /* Try to find commands that have special prefix in their reference names,
>
> Style?

Will fix.
>
> > +      * and mark them to run an external "proc-receive" hook later.
> > +      */

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 1/8] transport: not report a non-head push as a branch
  2020-04-12 20:26             ` Junio C Hamano
@ 2020-04-13 11:15               ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 11:15 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年4月13日周一 上午4:26写道:
> > +make_user_friendly_and_stable_output () {
> > +     make_user_friendly_and_stable_output_common | sed \
> > +             -e "s#To ../upstream.git#To <URL/of/upstream.git>#"
>
> Break the line immediately after the pipe, and begin the next line
> with the command.  That way, the shell knows that you haven't
> finished giving the command at the end of the first line, and you do
> not have to use a backslash, i.e.
>
>     make_user_friendly |
>     sed -e 's/from/to/'

Will do.

> > +             cd workbench &&
> > +             # Try to make a stable fixed width for abbreviated commit ID,
> > +             # this fixed-width oid will be replaced with "<OID>".
> > +             git config core.abbrev 7 &&
> > +             git remote add origin ../upstream.git &&
> > +             git update-ref refs/heads/master $A &&
> > +             git tag -m "v123" v123 $A &&
> > +             git push origin \
> > +                     $B:refs/heads/master \
> > +                     $A:refs/heads/next
> > +     ) &&
> > +     TAG=$(git -C workbench rev-parse v123) &&
> > +
> > +     # setup pre-receive hook
> > +     cat >upstream.git/hooks/pre-receive <<-\EOF &&
>
> Use write_script and you don't need the chmod at the end.

Yes, it's tedious to write chmod again and again.  Will replace all
the places for writing script.

> > +     #!/bin/sh
> > +
> > +     echo >&2 "# pre-receive hook"
> > +
> > +     while read old new ref
> > +     do
> > +             echo >&2 "pre-receive< $old $new $ref"
> > +     done
> > +     EOF
>
> Perhaps "exec >&2" upfront?

This works for "pre-receive" and "post-receive" script.  But in
"proc-receive" hook, stdout and stderr are different, and standard
output is used for protocol output.

> > diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
> > new file mode 100644
> > index 0000000000..6e400c0625
> > --- /dev/null
> > +++ b/t/t5411/common-functions.sh
> > @@ -0,0 +1,50 @@
>
> It is good that this omits "#!" and leaves the file without +x bit.
>
> > +# Format the output of git-push, git-show-ref and other commands to make a
> > +# user-friendly and stable text.  We can easily prepare the expect text
> > +# without having to worry about future changes of the commit ID and spaces
> > +# of the output.  We also replce single quotes with double quotes, because
> > +# it is boring to prepare unquoted single quotes in expect txt.
>
> s/txt/text/

Will do. Found it for a long time, but forgot to correct it.

> > +                              (starts_with(ref->name, "refs/tags/")
> > +                               ? "[new tag]"
> > +                               : (starts_with(ref->name, "refs/heads/")
> > +                                  ? "[new branch]"
> > +                                  : "[new reference]")),
> > +                              ref, ref->peer_ref, NULL, porcelain, summary_width);
>
> Now we say "outside refs/heads/ and refs/tags/, that's just a
> reference, different from branch".  OK.
>
> Do we ever update refs/remotes/ with this codepath?  Would we want
> to label it differently, i.e. as "remote-tracking branch"?

Can we call a reference (starts with "refs/remotes/") as a
remote-tracking branch, if it is on the remote side without a proper
remote and branch settings in .git/config?

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 3/8] refs.c: refactor to reuse ref_is_hidden()
  2020-04-12 21:38             ` Junio C Hamano
@ 2020-04-13 11:16               ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 11:16 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年4月13日周一 上午5:38写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> >
> > Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
>
> ref_is_matched() may not be wrong per-se, but I would imagine that
> ref_matches() would sound more natural.

Will do.


>
> The ancient varilabe hide_refs is probably misnamed and should be
> renamed when the codebase is quiescent to hidden_refs but of course
> we shouldn't do so before stablizing this series.
>

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 4/8] receive-pack: new config receive.procReceiveRefs
  2020-04-12 21:46             ` Junio C Hamano
@ 2020-04-13 11:16               ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 11:16 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年4月13日周一 上午5:46写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > +     if (strcmp(var, "receive.procreceiverefs") == 0) {
> > +             char *prefix;
> > +             int len;
> > +
> > +             if (!value)
> > +                     return config_error_nonbool(var);
> > +             prefix = xstrdup(value);
> > +             len = strlen(prefix);
> > +             while (len && prefix[len - 1] == '/')
> > +                     prefix[--len] = '\0';
> > +             string_list_insert(&proc_receive_refs, prefix);
>
> This smells like a copy of the hidden-refs configuration parsing,
> which uses string_list_append(), and its result is used for look-up

So this means I copied it from elsewhere ;-)
Will replace it from string_list_insert()  to string_list_append().

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v11 0/7] New proc-receive hook for centralized workflow
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 1/7] transport: not report a non-head push as a branch Jiang Xin
                               ` (6 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v10

* Patch 3/7 of v11 is a squash commit of patch 2/8 and patch 6/8 of v10.

* Fixed issues discovered for code review of v10.

## Range-diff v10...v11

1:  a293077bcf ! 1:  77a731c151 transport: not report a non-head push as a branch
    @@ t/t5411-proc-receive-hook.sh (new)
     +# we also replace the URL of different protocol for the upstream repository
     +# with a fixed pattern.
     +make_user_friendly_and_stable_output () {
    -+	make_user_friendly_and_stable_output_common | sed \
    -+		-e "s#To ../upstream.git#To <URL/of/upstream.git>#"
    ++	make_user_friendly_and_stable_output_common |
    ++		sed -e "s#To ../upstream.git#To <URL/of/upstream.git>#"
     +}
     +
     +# Refs of upstream : master(B)  next(A)
    @@ t/t5411-proc-receive-hook.sh (new)
     +	TAG=$(git -C workbench rev-parse v123) &&
     +
     +	# setup pre-receive hook
    -+	cat >upstream.git/hooks/pre-receive <<-\EOF &&
    -+	#!/bin/sh
    -+
    -+	echo >&2 "# pre-receive hook"
    -+
    ++	write_script upstream.git/hooks/pre-receive <<-\EOF &&
    ++	exec >&2
    ++	echo "# pre-receive hook"
     +	while read old new ref
     +	do
    -+		echo >&2 "pre-receive< $old $new $ref"
    ++		echo "pre-receive< $old $new $ref"
     +	done
     +	EOF
     +
     +	# setup post-receive hook
    -+	cat >upstream.git/hooks/post-receive <<-\EOF &&
    -+	#!/bin/sh
    -+
    -+	echo >&2 "# post-receive hook"
    -+
    ++	write_script upstream.git/hooks/post-receive <<-\EOF &&
    ++	exec >&2
    ++	echo "# post-receive hook"
     +	while read old new ref
     +	do
    -+		echo >&2 "post-receive< $old $new $ref"
    ++		echo "post-receive< $old $new $ref"
     +	done
     +	EOF
     +
    -+	chmod a+x \
    -+		upstream.git/hooks/pre-receive \
    -+		upstream.git/hooks/post-receive &&
    -+
     +	upstream=upstream.git
     +'
     +
    @@ t/t5411/common-functions.sh (new)
     +# user-friendly and stable text.  We can easily prepare the expect text
     +# without having to worry about future changes of the commit ID and spaces
     +# of the output.  We also replce single quotes with double quotes, because
    -+# it is boring to prepare unquoted single quotes in expect txt.
    ++# it is boring to prepare unquoted single quotes in expect text.
     +make_user_friendly_and_stable_output_common () {
     +	sed \
     +		-e "s/  *\$//" \
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +# we also replace the URL of different protocol for the upstream repository
     +# with a fixed pattern.
     +make_user_friendly_and_stable_output () {
    -+	make_user_friendly_and_stable_output_common | sed \
    -+		-e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
    ++	make_user_friendly_and_stable_output_common |
    ++		sed -e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
     +}
     +
     +# Refs of upstream : master(B)  next(A)
    @@ t/t5412-proc-receive-hook-http-protocol.sh (new)
     +	TAG=$(git -C workbench rev-parse v123) &&
     +
     +	# setup pre-receive hook
    -+	cat >upstream.git/hooks/pre-receive <<-\EOF &&
    -+	#!/bin/sh
    -+
    -+	echo >&2 "# pre-receive hook"
    -+
    ++	write_script upstream.git/hooks/pre-receive <<-\EOF &&
    ++	exec >&2
    ++	echo "# pre-receive hook"
     +	while read old new ref
     +	do
    -+		echo >&2 "pre-receive< $old $new $ref"
    ++		echo "pre-receive< $old $new $ref"
     +	done
     +	EOF
     +
     +	# setup post-receive hook
    -+	cat >upstream.git/hooks/post-receive <<-\EOF &&
    -+	#!/bin/sh
    -+
    -+	echo >&2 "# post-receive hook"
    -+
    ++	write_script upstream.git/hooks/post-receive <<-\EOF &&
    ++	exec >&2
    ++	echo "# post-receive hook"
     +	while read old new ref
     +	do
    -+		echo >&2 "post-receive< $old $new $ref"
    ++		echo "post-receive< $old $new $ref"
     +	done
     +	EOF
     +
    -+	chmod a+x \
    -+		upstream.git/hooks/pre-receive \
    -+		upstream.git/hooks/post-receive &&
    -+
     +	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
     +	mv upstream.git "$upstream" &&
     +	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
5:  75b178fc19 = 2:  4c4f7cffff connect: export parse_feature_value()
2:  915b9ef616 ! 3:  bef4360e58 receive-pack: add new proc-receive hook
    @@ Commit message
         other routines.  Finally, the result of the execution of these commands
         will be reported to end user.
     
    +    The reporting function from "receive-pack" to "send-pack" is also
    +    extended using a backward compatible way by adding key-value pairs
    +    after an null character, like:
    +
    +        # OK, run this command successfully with optional extended-status.
    +        ok <reference>\0ref=refs/pull/123/head old-oid=...
    +
    +        # NO, I reject it.
    +        ng <reference> <error message>
    +
    +    Parsing the extended status of the report lines for "send-pack" is not
    +    implemented in this commit, so we can make a compatible test on old
    +    version of a Git client.
    +
         Suggested-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
    @@ builtin/receive-pack.c: static void write_head_info(void)
      struct command {
      	struct command *next;
      	const char *error_string;
    ++	const char *extended_status;
      	unsigned int skip_update:1,
     -		     did_not_exist:1;
     +		     did_not_exist:1,
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +{
     +	struct command *cmd;
     +	struct command *hint = NULL;
    ++	struct strbuf extended_status = STRBUF_INIT;
     +	int code = 0;
     +
     +	for (;;) {
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		const char *p;
     +		char *status;
     +		char *msg = NULL;
    ++		int len;
     +
     +		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
     +			break;
    ++		len = strlen(reader->line);
     +		if (parse_oid_hex(reader->line, &old_oid, &p) ||
     +		    *p++ != ' ' ||
     +		    parse_oid_hex(p, &new_oid, &p) ||
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			continue;
     +		}
     +		*status++ = '\0';
    -+		if (strlen(status) > 2 && *(status + 2) == ' ') {
    -+			msg = status + 2;
    ++		msg = strchr(status, ' ');
    ++		if (msg)
     +			*msg++ = '\0';
    -+		}
    -+		if (strlen(status) != 2) {
    -+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    -+				    status, reader->line);
    -+			code = -1;
    -+			continue;
    -+		}
     +
     +		/* first try searching at our hint, falling back to all refs */
     +		if (hint)
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			continue;
     +		}
     +		if (!hint->run_proc_receive) {
    -+			strbuf_addf(errmsg, "proc-receive reported status on ref of builtin command: %s\n",
    ++			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
     +				    refname);
     +			continue;
     +		}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			else
     +				hint->error_string = "failed";
     +			code = -1;
    ++		} else if (!strcmp("ft", status)) {
    ++			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
    ++			hint->run_proc_receive = 0;
     +		} else if (strcmp("ok", status)) {
    -+			strbuf_addf(errmsg, "proc-receive has bad status '%s' for '%s'\n",
    -+				    status, reader->line);
    ++			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
    ++				    status, refname);
     +			code = -1;
     +			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
     +			continue;
     +		}
    -+		oidcpy(&hint->old_oid, &old_oid);
    -+		oidcpy(&hint->new_oid, &new_oid);
    -+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
    ++		if (reader->pktlen > len)
    ++			strbuf_addstr(&extended_status, (char *)reader->line + len + 1);
    ++		if (oidcmp(&hint->old_oid, &old_oid)) {
    ++			oidcpy(&hint->old_oid, &old_oid);
    ++			strbuf_addf(&extended_status, "%sold-oid=%s",
    ++				    extended_status.len > 0 ? " ": "",
    ++				    oid_to_hex(&old_oid));
    ++		}
    ++		if (oidcmp(&hint->new_oid, &new_oid)) {
    ++			oidcpy(&hint->new_oid, &new_oid);
    ++			strbuf_addf(&extended_status, "%snew-oid=%s",
    ++				    extended_status.len > 0 ? " ": "",
    ++				    oid_to_hex(&new_oid));
    ++		}
    ++		if (extended_status.len > 0)
    ++			hint->extended_status = strbuf_detach(&extended_status, NULL);
    ++		if (hint->run_proc_receive)
    ++			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
     +	}
     +
     +	for (cmd = commands; cmd; cmd = cmd->next)
     +		if (cmd->run_proc_receive && !cmd->error_string &&
     +		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
    -+		    cmd->error_string = "no report from proc-receive";
    ++		    cmd->error_string = "proc-receive failed to report status";
     +		    code = -1;
     +		}
     +
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      
      	reject_updates_to_hidden(commands);
      
    -+	/* Try to find commands that have special prefix in their reference names,
    ++	/*
    ++	 * Try to find commands that have special prefix in their reference names,
     +	 * and mark them to run an external "proc-receive" hook later.
     +	 */
     +	for (cmd = commands; cmd; cmd = cmd->next) {
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      	if (use_atomic)
      		execute_commands_atomic(commands, si);
      	else
    +@@ builtin/receive-pack.c: static void report(struct command *commands, const char *unpack_status)
    + 	packet_buf_write(&buf, "unpack %s\n",
    + 			 unpack_status ? unpack_status : "ok");
    + 	for (cmd = commands; cmd; cmd = cmd->next) {
    +-		if (!cmd->error_string)
    +-			packet_buf_write(&buf, "ok %s\n",
    +-					 cmd->ref_name);
    +-		else
    +-			packet_buf_write(&buf, "ng %s %s\n",
    +-					 cmd->ref_name, cmd->error_string);
    ++		if (!cmd->error_string) {
    ++			if (cmd->extended_status)
    ++				packet_buf_write(&buf, "ok %s%c%s\n",
    ++						 cmd->ref_name, '\0',
    ++						 cmd->extended_status);
    ++			else
    ++				packet_buf_write(&buf, "ok %s\n",
    ++						 cmd->ref_name);
    ++		} else {
    ++			if (cmd->extended_status)
    ++				packet_buf_write(&buf, "ng %s %s%c%s\n",
    ++						 cmd->ref_name, cmd->error_string,
    ++						 '\0', cmd->extended_status);
    ++			else
    ++				packet_buf_write(&buf, "ng %s %s\n",
    ++						 cmd->ref_name, cmd->error_string);
    ++		}
    + 	}
    + 	packet_buf_flush(&buf);
    + 
     
      ## t/helper/test-proc-receive.c (new) ##
     @@
    @@ t/helper/test-proc-receive.c (new)
     +	}
     +
     +	if (returns.nr)
    -+		for_each_string_list_item(item, &returns)
    -+			packet_write_fmt(1, "%s\n", item->string);
    ++		for_each_string_list_item(item, &returns) {
    ++			char *p;
    ++
    ++			p = strstr(item->string, "\\0");
    ++			if (p) {
    ++				*p = '\0';
    ++				p += 2;
    ++				packet_write_fmt(1, "%s%c%s\n", item->string, '\0', p);
    ++			} else {
    ++				packet_write_fmt(1, "%s\n", item->string);
    ++			}
    ++		}
     +	packet_flush(1);
     +	sigchain_pop(SIGPIPE);
     +
    @@ t/t5411/common-functions.sh: create_commits_in () {
      # user-friendly and stable text.  We can easily prepare the expect text
      # without having to worry about future changes of the commit ID and spaces
     -# of the output.  We also replce single quotes with double quotes, because
    --# it is boring to prepare unquoted single quotes in expect txt.
    +-# it is boring to prepare unquoted single quotes in expect text.
     +# of the output.  Single quotes are replaced with double quotes, because
    -+# it is boring to prepare unquoted single quotes in expect txt.  We also
    ++# it is boring to prepare unquoted single quotes in expect text.  We also
     +# remove some locale error messages, which break test if we turn on
     +# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
     +# on plumbing commands.
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       next(A)  refs/for/master/topic(A)
    -+test_expect_success "no proc-receive hook, fail to push special ref" '
    ++test_expect_success "proc-receive: no hook, fail to push special ref" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:next \
     +		HEAD:refs/for/master/topic \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
    -+test_expect_success "no proc-receive hook, fail all for atomic push" '
    ++test_expect_success "proc-receive: no hook, fail all for atomic push" '
     +	test_must_fail git -C workbench push --atomic origin \
     +		$B:master \
     +		HEAD:next \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (bad version)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
    -+	#!/bin/sh
    -+
    ++test_expect_success "setup proc-receive hook (unknown version)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v --version 2
     +	EOF
    -+	chmod a+x "$upstream/hooks/proc-receive"
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive bad protocol: unknown version" '
    ++test_expect_success "proc-receive: bad protocol (unknown version)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (no report)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v
     +	EOF
     +'
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       next(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive bad protocol: no report" '
    ++test_expect_success "proc-receive: bad protocol (no report)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/heads/next \
     +		HEAD:refs/for/master/topic >out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	To <URL/of/upstream.git>
     +	 * [new branch] HEAD -> next
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (bad oid)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
     +		-r "bad-id new-id ref ok"
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive bad protocol: bad oid" '
    ++test_expect_success "proc-receive: bad protocol (bad oid)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic\
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive> bad-id new-id ref ok
     +	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
     +	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (no status)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/for/master/topic"
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive bad protocol: no status" '
    ++test_expect_success "proc-receive: bad protocol (no status)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
     +	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (unknown status)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/for/master/topic xx msg"
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive bad protocol: unknown status" '
    ++test_expect_success "proc-receive: bad protocol (unknown status)" '
     +	test_must_fail git -C workbench push origin \
     +			HEAD:refs/for/master/topic \
     +			>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
    -+	remote: error: proc-receive has bad status "xx" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
     +	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    -+	EOF
    -+	test_cmp expect actual &&
    -+	git -C "$upstream" show-ref >out &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	<COMMIT-A> refs/heads/master
    -+	EOF
    -+	test_cmp expect actual
    -+'
    -+
    -+test_expect_success "setup proc-receive hook (bad status)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    -+	printf >&2 "# proc-receive hook\n"
    -+
    -+	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/master/topic bad status"
    -+	EOF
    -+'
    -+
    -+# Refs of upstream : master(A)
    -+# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive bad protocol: bad status" '
    -+	test_must_fail git -C workbench push origin \
    -+		HEAD:refs/for/master/topic \
    -+		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic bad status
    -+	remote: error: proc-receive has bad status "bad status" for "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    -+	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (ng)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/for/master/topic ng"
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: fail to update (no message)" '
    ++test_expect_success "proc-receive: fail to update (ng)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +'
     +
     +test_expect_success "setup proc-receive hook (ng message)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: fail to update (has message)" '
    ++test_expect_success "proc-receive: fail to update (ng message)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (report status on builtin command)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++test_expect_success "setup proc-receive hook (unexpected ref)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/heads/master ok"
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         : (B)                   refs/for/master/topic
    -+test_expect_success "proc-receive: warning on report for builtin command" '
    ++test_expect_success "proc-receive: report on unexpected ref" '
     +	test_must_fail git -C workbench push origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/for/master/topic \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
    -+	remote: error: proc-receive reported status on ref of builtin command: refs/heads/master
    ++	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	To <URL/of/upstream.git>
     +	 <OID-A>..<OID-B> <COMMIT-B> -> master
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (no report from proc-receive)
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    ++# Refs of upstream : master(B)
    ++# Refs of workbench: master(A)  tags/v123
     +test_expect_success "cleanup" '
     +	git -C "$upstream" update-ref refs/heads/master $A
     +'
     +
     +test_expect_success "setup proc-receive hook (ok)" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
     +		-r "$ZERO_OID $A refs/for/master/topic ok"
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/a/b/c/my/topic
    -+test_expect_success "proc-receive: no report from proc-receive" '
    ++test_expect_success "proc-receive: failed to report status" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/a/b/c/my/topic \
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
     +	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
     +	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (no report from proc-receive)
    ++	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -o ...  :                       refs/for/master/topic
    -+test_expect_success "not support push options" '
    ++test_expect_success "proc-receive: not support push options" '
     +	test_must_fail git -C workbench push \
     +		-o issue=123 \
     +		-o reviewer=user1 \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -o ...  :                       next(A)  refs/for/master/topic
    -+test_expect_success "push with options" '
    ++test_expect_success "proc-receive: push with options" '
     +	git -C workbench push \
     +		--atomic \
     +		-o issue=123 \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	EOF
     +	test_cmp expect actual
     +'
    ++
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup" '
    ++	git -C "$upstream" update-ref -d refs/heads/next
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (ft)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "$ZERO_OID $B refs/for/master/topic ft"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(B)
    ++test_expect_success "proc-receive: fall throught, let receive-pack to execute" '
    ++	git -C workbench push origin \
    ++		$B:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-B> refs/for/master/topic ft
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] <COMMIT-B> -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-B> refs/for/master/topic
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(A)             refs/for/master/topic(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup" '
    ++	git -C "$upstream" update-ref -d refs/for/master/topic
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (with extended status)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "$ZERO_OID $A refs/for/a/b/c/topic ok" \
    ++		-r "$ZERO_OID $A refs/for/next/topic ok\0ref=refs/pull/123/head" \
    ++		-r "$B $A refs/for/master/topic ok\0ref=refs/pull/124/head forced-update"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report with extended status" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/next/topic \
    ++		HEAD:refs/for/a/b/c/topic \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic ok
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/123/head
    ++	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok\0ref=refs/pull/124/head forced-update
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/next/topic
    ++	 * [new reference] HEAD -> refs/for/a/b/c/topic
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "$ZERO_OID $A refs/for/next/topic ok" \
    ++		-r "$A $B refs/for/master/topic ok"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report update of mixed refs" '
    ++	git -C workbench push origin \
    ++		$B:refs/heads/master \
    ++		HEAD:refs/heads/bar \
    ++		HEAD:refs/heads/baz \
    ++		HEAD:refs/for/next/topic \
    ++		HEAD:refs/heads/foo \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    ++	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok
    ++	remote: # post-receive hook
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 <OID-A>..<OID-B> <COMMIT-B> -> master
    ++	 * [new branch] HEAD -> bar
    ++	 * [new branch] HEAD -> baz
    ++	 * [new reference] HEAD -> refs/for/next/topic
    ++	 * [new branch] HEAD -> foo
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/bar
    ++	<COMMIT-A> refs/heads/baz
    ++	<COMMIT-A> refs/heads/foo
    ++	<COMMIT-B> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
3:  16257616ec < -:  ---------- refs.c: refactor to reuse ref_is_hidden()
7:  d1677b61d1 ! 4:  a4a1cc1d14 send-pack: extension for client-side status report
    @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref
      	}
     
      ## t/t5411/common-test-cases.sh ##
    -@@ t/t5411/common-test-cases.sh: test_expect_success "report update of all special refs" '
    - 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    +@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report with extended status" '
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
      	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
      	To <URL/of/upstream.git>
     -	 * [new reference] HEAD -> refs/for/next/topic
     +	 * [new reference] HEAD -> refs/pull/123/head
    - 	 * [new reference] HEAD -> refs/review/a/b/c/topic
    + 	 * [new reference] HEAD -> refs/for/a/b/c/topic
     -	 * [new reference] HEAD -> refs/for/master/topic
     +	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
      	EOF
      	test_cmp expect actual &&
      	git -C "$upstream" show-ref >out &&
    -@@ t/t5411/common-test-cases.sh: test_expect_success "report mixed refs update" '
    +@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update of mixed refs" '
      	 * [new branch] HEAD -> baz
      	 * [new reference] HEAD -> refs/for/next/topic
      	 * [new branch] HEAD -> foo
    @@ t/t5411/common-test-cases.sh: test_expect_success "report mixed refs update" '
      	EOF
      	test_cmp expect actual &&
      	git -C "$upstream" show-ref >out &&
    -@@ t/t5411/common-test-cases.sh: test_expect_success "report test: fallthrough" '
    - 	 <OID-A>..<OID-B> <COMMIT-B> -> bar
    - 	 - [deleted] foo
    - 	 + <OID-B>...<OID-A> HEAD -> master (forced update)
    --	 * [new reference] HEAD -> refs/for/master/topic
    --	 * [new reference] HEAD -> refs/for/next/topic
    -+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
    -+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
    - 	EOF
    - 	test_cmp expect actual &&
    - 	git -C "$upstream" show-ref >out &&
     
      ## transport-helper.c ##
     @@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
-:  ---------- > 5:  4e3c383c12 refs.c: refactor to reuse ref_is_hidden()
4:  cd49b57c17 ! 6:  dd83569c4e receive-pack: new config receive.procReceiveRefs
    @@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const ch
     +		len = strlen(prefix);
     +		while (len && prefix[len - 1] == '/')
     +			prefix[--len] = '\0';
    -+		string_list_insert(&proc_receive_refs, prefix);
    ++		string_list_append(&proc_receive_refs, prefix);
     +		return 0;
     +	}
     +
    @@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const ch
      }
      
     @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
    - 	/* Try to find commands that have special prefix in their reference names,
    + 	 * Try to find commands that have special prefix in their reference names,
      	 * and mark them to run an external "proc-receive" hook later.
      	 */
     -	for (cmd = commands; cmd; cmd = cmd->next) {
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +	if (proc_receive_refs.nr > 0) {
     +		struct strbuf refname_full = STRBUF_INIT;
     +		size_t prefix_len;
    -+
    -+		strbuf_addstr(&refname_full, get_git_namespace());
    -+		prefix_len = refname_full.len;
      
     -		/* TODO: replace the fixed prefix by looking up git config variables. */
     -		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
     -			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     -			run_proc_receive = 1;
    ++		strbuf_addstr(&refname_full, get_git_namespace());
    ++		prefix_len = refname_full.len;
    ++
     +		for (cmd = commands; cmd; cmd = cmd->next) {
     +			if (!should_process_cmd(cmd))
     +				continue;
     +
     +			strbuf_setlen(&refname_full, prefix_len);
     +			strbuf_addstr(&refname_full, cmd->ref_name);
    -+			if (ref_is_matched(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
    ++			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
     +				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     +				run_proc_receive = 1;
     +			}
    @@ t/t5411/common-test-cases.sh: test_expect_success "cleanup" '
      # Refs of upstream : master(A)
      # Refs of workbench: master(A)  tags/v123
      # git push         :                       next(A)  refs/for/master/topic(A)
    -@@ t/t5411/common-test-cases.sh: test_expect_success "push with options" '
    - 	EOF
    - 	test_cmp expect actual
    - '
    -+
    -+# Refs of upstream : master(A)             next(A)
    -+# Refs of workbench: master(A)  tags/v123
    -+test_expect_success "cleanup" '
    -+	git -C "$upstream" update-ref -d refs/heads/next
    -+'
    -+
    -+test_expect_success "setup proc-receive hook" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
    -+	#!/bin/sh
    -+
    -+	printf >&2 "# proc-receive hook\n"
    -+
    -+	test-tool proc-receive -v \
    +@@ t/t5411/common-test-cases.sh: test_expect_success "setup proc-receive hook (with extended status)" '
    + 	write_script "$upstream/hooks/proc-receive" <<-EOF
    + 	printf >&2 "# proc-receive hook\n"
    + 	test-tool proc-receive -v \
    +-		-r "$ZERO_OID $A refs/for/a/b/c/topic ok" \
     +		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
    -+		-r "$ZERO_OID $A refs/for/next/topic ok" \
    -+		-r "$B $A refs/for/master/topic ok"
    -+	EOF
    -+	chmod a+x "$upstream/hooks/proc-receive"
    -+'
    -+
    -+# Refs of upstream : master(A)
    -+# Refs of workbench: master(A)  tags/v123
    + 		-r "$ZERO_OID $A refs/for/next/topic ok\0ref=refs/pull/123/head" \
    + 		-r "$B $A refs/for/master/topic ok\0ref=refs/pull/124/head forced-update"
    + 	EOF
    +@@ t/t5411/common-test-cases.sh: test_expect_success "setup proc-receive hook (with extended status)" '
    + 
    + # Refs of upstream : master(A)
    + # Refs of workbench: master(A)  tags/v123
    +-# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
     +# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "report update of all special refs" '
    -+	git -C workbench push origin \
    -+		HEAD:refs/for/next/topic \
    + test_expect_success "proc-receive: report with extended status" '
    + 	git -C workbench push origin \
    + 		HEAD:refs/for/next/topic \
    +-		HEAD:refs/for/a/b/c/topic \
     +		HEAD:refs/review/a/b/c/topic \
    -+		HEAD:refs/for/master/topic \
    -+		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    + 		HEAD:refs/for/master/topic \
    + 		>out 2>&1 &&
    + 	make_user_friendly_and_stable_output <out >actual &&
    + 	cat >expect <<-EOF &&
    + 	remote: # pre-receive hook
    + 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    +-	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    + 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	remote: # proc-receive hook
    + 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    +-	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    +-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic ok
     +	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    -+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok
    -+	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    + 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/123/head
    + 	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok\0ref=refs/pull/124/head forced-update
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    +-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    -+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
    -+	To <URL/of/upstream.git>
    -+	 * [new reference] HEAD -> refs/for/next/topic
    + 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    + 	 * [new reference] HEAD -> refs/pull/123/head
    +-	 * [new reference] HEAD -> refs/for/a/b/c/topic
     +	 * [new reference] HEAD -> refs/review/a/b/c/topic
    -+	 * [new reference] HEAD -> refs/for/master/topic
    -+	EOF
    -+	test_cmp expect actual &&
    -+	git -C "$upstream" show-ref >out &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	<COMMIT-A> refs/heads/master
    -+	EOF
    -+	test_cmp expect actual
    + 	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
    + 	EOF
    + 	test_cmp expect actual &&
    +@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update of mixed refs" '
    + 	EOF
    + 	test_cmp expect actual
    + '
    ++
    ++test_expect_success "config add receive.procReceiveRefs = refs" '
    ++	git -C "$upstream" config --add receive.procReceiveRefs refs
     +'
     +
     +test_expect_success "setup proc-receive hook" '
    -+	cat >"$upstream/hooks/proc-receive" <<-EOF &&
    -+	#!/bin/sh
    -+
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/next/topic ok" \
    -+		-r "$A $B refs/for/master/topic ok"
    ++		-r "$B $A refs/heads/master ft" \
    ++		-r "$A $ZERO_OID refs/heads/foo ft" \
    ++		-r "$A $B refs/heads/bar ft" \
    ++		-r "$A $B refs/for/master/topic ok\0ref=refs/pull/123/head" \
    ++		-r "$B $A refs/for/next/topic ok\0ref=refs/pull/124/head forced-update"
     +	EOF
    -+	chmod a+x "$upstream/hooks/proc-receive"
     +'
     +
    -+# Refs of upstream : master(A)
    ++# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
    -+test_expect_success "report mixed refs update" '
    -+	git -C workbench push origin \
    -+		HEAD:refs/heads/bar \
    -+		HEAD:refs/heads/baz \
    -+		HEAD:refs/for/next/topic \
    -+		HEAD:refs/heads/foo \
    ++# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
    ++test_expect_success "proc-receive: process all refs" '
    ++	git -C workbench push -f origin \
    ++		HEAD:refs/heads/master \
    ++		:refs/heads/foo \
    ++		$B:refs/heads/bar \
     +		HEAD:refs/for/master/topic \
    ++		HEAD:refs/for/next/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
    ++	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
    ++	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
    ++	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
    ++	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    -+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
    ++	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
    ++	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
    ++	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok\0ref=refs/pull/123/head
    ++	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/124/head forced-update
     +	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
    ++	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
    ++	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
    ++	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
     +	To <URL/of/upstream.git>
    -+	 * [new branch] HEAD -> bar
    -+	 * [new branch] HEAD -> baz
    -+	 * [new reference] HEAD -> refs/for/next/topic
    -+	 * [new branch] HEAD -> foo
    -+	 * [new reference] HEAD -> refs/for/master/topic
    ++	 <OID-A>..<OID-B> <COMMIT-B> -> bar
    ++	 - [deleted] foo
    ++	 + <OID-B>...<OID-A> HEAD -> master (forced update)
    ++	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
    ++	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	<COMMIT-A> refs/heads/bar
    ++	<COMMIT-B> refs/heads/bar
     +	<COMMIT-A> refs/heads/baz
    -+	<COMMIT-A> refs/heads/foo
     +	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
6:  860860536e < -:  ---------- receive-pack: extension for server-side report
8:  41159c2ef2 = 7:  2d04f495a3 doc: add documentation for the proc-receive hook

---

Jiang Xin (7):
  transport: not report a non-head push as a branch
  connect: export parse_feature_value()
  receive-pack: add new proc-receive hook
  send-pack: extension for client-side status report
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt           |  14 +
 Documentation/githooks.txt                 |  58 ++
 Makefile                                   |   1 +
 builtin/receive-pack.c                     | 328 ++++++++-
 connect.c                                  |   3 +-
 connect.h                                  |   1 +
 refs.c                                     |  13 +-
 refs.h                                     |   1 +
 remote.c                                   |  28 +
 remote.h                                   |   3 +
 send-pack.c                                |  13 +-
 t/helper/test-proc-receive.c               | 178 +++++
 t/helper/test-tool.c                       |   1 +
 t/helper/test-tool.h                       |   1 +
 t/t5411-proc-receive-hook.sh               |  67 ++
 t/t5411/common-functions.sh                |  54 ++
 t/t5411/common-test-cases.sh               | 794 +++++++++++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh |  78 ++
 t/t5516-fetch-push.sh                      |   2 +-
 transport-helper.c                         |  66 +-
 transport.c                                |  42 +-
 21 files changed, 1691 insertions(+), 55 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v11 1/7] transport: not report a non-head push as a branch
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 0/7] " Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 2/7] connect: export parse_feature_value() Jiang Xin
                               ` (5 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh               | 67 +++++++++++++++++++
 t/t5411/common-functions.sh                | 50 ++++++++++++++
 t/t5411/common-test-cases.sh               | 43 ++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh | 78 ++++++++++++++++++++++
 t/t5516-fetch-push.sh                      |  2 +-
 transport.c                                |  9 ++-
 6 files changed, 245 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..3f4bee76d9
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace the URL of different protocol for the upstream repository
+# with a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common |
+		sed -e "s#To ../upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	write_script upstream.git/hooks/pre-receive <<-\EOF &&
+	exec >&2
+	echo "# pre-receive hook"
+	while read old new ref
+	do
+		echo "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	write_script upstream.git/hooks/post-receive <<-\EOF &&
+	exec >&2
+	echo "# post-receive hook"
+	while read old new ref
+	do
+		echo "post-receive< $old $new $ref"
+	done
+	EOF
+
+	upstream=upstream.git
+'
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..4337b10c8c
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,50 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.
+make_user_friendly_and_stable_output_common () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
+}
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
new file mode 100644
index 0000000000..23655846e4
--- /dev/null
+++ b/t/t5411/common-test-cases.sh
@@ -0,0 +1,43 @@
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
new file mode 100755
index 0000000000..d102bc438b
--- /dev/null
+++ b/t/t5412-proc-receive-hook-http-protocol.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook for HTTP protocol'
+
+. ./test-lib.sh
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace the URL of different protocol for the upstream repository
+# with a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common |
+		sed -e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git -C upstream.git config http.receivepack true &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	write_script upstream.git/hooks/pre-receive <<-\EOF &&
+	exec >&2
+	echo "# pre-receive hook"
+	while read old new ref
+	do
+		echo "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	write_script upstream.git/hooks/post-receive <<-\EOF &&
+	exec >&2
+	echo "# post-receive hook"
+	while read old new ref
+	do
+		echo "post-receive< $old $new $ref"
+	done
+	EOF
+
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..272c0f4046 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v11 2/7] connect: export parse_feature_value()
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 0/7] " Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 1/7] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 3/7] receive-pack: add new proc-receive hook Jiang Xin
                               ` (4 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Export `parse_feature_value()` to parse extended status of report for
"proc-receive" and "receive-pack".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 connect.c | 3 +--
 connect.h | 1 +
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/connect.c b/connect.c
index b6451ab5e8..08e0616726 100644
--- a/connect.c
+++ b/connect.c
@@ -18,7 +18,6 @@
 
 static char *server_capabilities_v1;
 static struct argv_array server_capabilities_v2 = ARGV_ARRAY_INIT;
-static const char *parse_feature_value(const char *, const char *, int *);
 
 static int check_ref(const char *name, unsigned int flags)
 {
@@ -447,7 +446,7 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
 	return list;
 }
 
-static const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
+const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
 {
 	int len;
 
diff --git a/connect.h b/connect.h
index 5f2382e018..17b3252cbc 100644
--- a/connect.h
+++ b/connect.h
@@ -12,6 +12,7 @@ int finish_connect(struct child_process *conn);
 int git_connection_is_socket(struct child_process *conn);
 int server_supports(const char *feature);
 int parse_feature_request(const char *features, const char *feature);
+const char *parse_feature_value(const char *feature_list, const char *feature, int *len_ret);
 const char *server_feature_value(const char *feature, int *len_ret);
 int url_is_local_not_ssh(const char *url);
 
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v11 3/7] receive-pack: add new proc-receive hook
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
                               ` (2 preceding siblings ...)
  2020-04-13 16:48             ` [PATCH v11 2/7] connect: export parse_feature_value() Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 4/7] send-pack: extension for client-side status report Jiang Xin
                               ` (3 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
    # NO, I reject it.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status are given in key=value pairs after the null
    # character.
    H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
                forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in key-value pairs as extended status of the report
line.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" is also
extended using a backward compatible way by adding key-value pairs
after an null character, like:

    # OK, run this command successfully with optional extended-status.
    ok <reference>\0ref=refs/pull/123/head old-oid=...

    # NO, I reject it.
    ng <reference> <error message>

Parsing the extended status of the report lines for "send-pack" is not
implemented in this commit, so we can make a compatible test on old
version of a Git client.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 299 +++++++++++++++-
 t/helper/test-proc-receive.c | 178 +++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411/common-functions.sh  |  10 +-
 t/t5411/common-test-cases.sh | 673 +++++++++++++++++++++++++++++++++++
 7 files changed, 1151 insertions(+), 12 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..fd2dc23c2e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,15 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extended_status;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +821,250 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	struct strbuf extended_status = STRBUF_INIT;
+	int code = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *refname;
+		const char *p;
+		char *status;
+		char *msg = NULL;
+		int len;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		len = strlen(reader->line);
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ') {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+
+		refname = p;
+		status = strchr(p, ' ');
+		if (!status) {
+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+		*status++ = '\0';
+		msg = strchr(status, ' ');
+		if (msg)
+			*msg++ = '\0';
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!strcmp(status, "ng")) {
+			if (msg)
+				hint->error_string = xstrdup(msg);
+			else
+				hint->error_string = "failed";
+			code = -1;
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else if (strcmp("ok", status)) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    status, refname);
+			code = -1;
+			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
+			continue;
+		}
+		if (reader->pktlen > len)
+			strbuf_addstr(&extended_status, (char *)reader->line + len + 1);
+		if (oidcmp(&hint->old_oid, &old_oid)) {
+			oidcpy(&hint->old_oid, &old_oid);
+			strbuf_addf(&extended_status, "%sold-oid=%s",
+				    extended_status.len > 0 ? " ": "",
+				    oid_to_hex(&old_oid));
+		}
+		if (oidcmp(&hint->new_oid, &new_oid)) {
+			oidcpy(&hint->new_oid, &new_oid);
+			strbuf_addf(&extended_status, "%snew-oid=%s",
+				    extended_status.len > 0 ? " ": "",
+				    oid_to_hex(&new_oid));
+		}
+		if (extended_status.len > 0)
+			hint->extended_status = strbuf_detach(&extended_status, NULL);
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->error_string &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "proc-receive failed to report status";
+		    code = -1;
+		}
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1640,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1680,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1716,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1746,21 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1787,12 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+				cmd->error_string = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -1899,12 +2169,23 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
-			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+		if (!cmd->error_string) {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, '\0',
+						 cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+		} else {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ng %s %s%c%s\n",
+						 cmd->ref_name, cmd->error_string,
+						 '\0', cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ng %s %s\n",
+						 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..c9f5c5aae4
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,178 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns) {
+			char *p;
+
+			p = strstr(item->string, "\\0");
+			if (p) {
+				*p = '\0';
+				p += 2;
+				packet_write_fmt(1, "%s%c%s\n", item->string, '\0', p);
+			} else {
+				packet_write_fmt(1, "%s\n", item->string);
+			}
+		}
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 4337b10c8c..1fbd24dbaf 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -34,8 +34,11 @@ create_commits_in () {
 # Format the output of git-push, git-show-ref and other commands to make a
 # user-friendly and stable text.  We can easily prepare the expect text
 # without having to worry about future changes of the commit ID and spaces
-# of the output.  We also replce single quotes with double quotes, because
-# it is boring to prepare unquoted single quotes in expect text.
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
 make_user_friendly_and_stable_output_common () {
 	sed \
 		-e "s/  *\$//" \
@@ -46,5 +49,6 @@ make_user_friendly_and_stable_output_common () {
 		-e "s/$TAG/<TAG-v123>/g" \
 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
-		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "/^error: / d"
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 23655846e4..fe970df755 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -41,3 +41,676 @@ test_expect_success "normal git-push command" '
 	EOF
 	test_cmp expect actual
 '
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown version)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { s/   */ /g; p; }" \
+		-e "/^ / { s/   */ /g; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	sed -n -e "/^remote: error:/ { s/   */ /g; p; }" \
+		<actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (bad oid)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "bad-id new-id ref ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (bad oid)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> bad-id new-id ref ok
+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no status)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no status)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unexpected ref)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/heads/master ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report on unexpected ref" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: failed to report status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook (ft)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $B refs/for/master/topic ft"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-B> refs/for/master/topic ft
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
+
+test_expect_success "setup proc-receive hook (with extended status)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/for/next/topic ok\0ref=refs/pull/123/head" \
+		-r "$B $A refs/for/master/topic ok\0ref=refs/pull/124/head forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with extended status" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok\0ref=refs/pull/124/head forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$ZERO_OID $A refs/for/next/topic ok" \
+		-r "$A $B refs/for/master/topic ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v11 4/7] send-pack: extension for client-side status report
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
                               ` (3 preceding siblings ...)
  2020-04-13 16:48             ` [PATCH v11 3/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                               ` (2 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This is the client-side extension for status report of git-push.

The "proc-receive" hook may receive a command for a pseudo-reference
with a zero-old as its old-oid, while the result of the hook may point
to an alternate reference and the reference may exist already with a
non-zero old-oid.  Git client may receive a report with extened status
as the following example:

    ok refs/for/master/topic\0ref=refs/pull/123/head old-oid=...

Parse the key-value pairs in the extended status after the null
character of the report, and show the proper reference name, old-oid
and new-oid to the user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 remote.c                     | 28 +++++++++++++++
 remote.h                     |  3 ++
 send-pack.c                  | 13 ++++++-
 t/t5411/common-test-cases.sh |  6 ++--
 transport-helper.c           | 66 +++++++++++++++++++-----------------
 transport.c                  | 33 +++++++++++++++---
 6 files changed, 110 insertions(+), 39 deletions(-)

diff --git a/remote.c b/remote.c
index c43196ec06..0b22d9e14c 100644
--- a/remote.c
+++ b/remote.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "config.h"
+#include "connect.h"
 #include "remote.h"
 #include "refs.h"
 #include "refspec.h"
@@ -2311,3 +2312,30 @@ void apply_push_cas(struct push_cas_option *cas,
 	for (ref = remote_refs; ref; ref = ref->next)
 		apply_cas(cas, remote, ref);
 }
+
+void update_ref_from_remote_status(struct ref *ref)
+{
+	char *val;
+	char c;
+	int len;
+
+	if (!ref->remote_status)
+		return;
+
+	val = (char *)parse_feature_value(ref->remote_status, "old-oid", &len);
+	if (val && len) {
+		c = *(val+len);
+		*(val+len) = '\0';
+		get_oid_hex(val, &ref->old_oid);
+		*(val+len) = c;
+	}
+	val = (char *)parse_feature_value(ref->remote_status, "new-oid", &len);
+	if (val && len) {
+		c = *(val+len);
+		*(val+len) = '\0';
+		get_oid_hex(val, &ref->new_oid);
+		*(val+len) = c;
+	}
+	if (parse_feature_request(ref->remote_status, "forced-update"))
+		ref->forced_update = 1;
+}
diff --git a/remote.h b/remote.h
index 11d8719b58..09cf7014e3 100644
--- a/remote.h
+++ b/remote.h
@@ -345,4 +345,7 @@ int parseopt_push_cas_option(const struct option *, const char *arg, int unset);
 int is_empty_cas(const struct push_cas_option *);
 void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
 
+/* Parse key-value pairs of remote_status and update the reference accordingly */
+void update_ref_from_remote_status(struct ref *ref);
+
 #endif
diff --git a/send-pack.c b/send-pack.c
index 0407841ae8..ab294c9426 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -159,6 +159,9 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 	while (1) {
 		const char *refname;
 		char *msg;
+		char *extended_status = NULL;
+		int len;
+
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
@@ -167,10 +170,13 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			break;
 		}
 
+		len = strlen(reader->line);
 		refname = reader->line + 3;
 		msg = strchr(refname, ' ');
 		if (msg)
 			*msg++ = '\0';
+		if (reader->pktlen > len)
+			extended_status = (char *)reader->line + len + 1;
 
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
@@ -194,7 +200,12 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			hint->status = REF_STATUS_REMOTE_REJECT;
 			ret = -1;
 		}
-		hint->remote_status = xstrdup_or_null(msg);
+		if (msg) {
+			hint->remote_status = xstrdup(msg);
+		} else if (extended_status) {
+			hint->remote_status = xstrdup(extended_status);
+			update_ref_from_remote_status(hint);
+		}
 		/* start our next search from the next ref */
 		hint = hint->next;
 	}
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index fe970df755..7fa7211ce4 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -640,9 +640,9 @@ test_expect_success "proc-receive: report with extended status" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -701,7 +701,7 @@ test_expect_success "proc-receive: report update of mixed refs" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..d2179631cf 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
@@ -802,6 +804,8 @@ static int push_update_ref_status(struct strbuf *buf,
 	(*ref)->status = status;
 	(*ref)->forced_update |= forced;
 	(*ref)->remote_status = msg;
+	if (status == REF_STATUS_OK && (*ref)->remote_status)
+		update_ref_from_remote_status(*ref);
 	return !(status == REF_STATUS_OK);
 }
 
diff --git a/transport.c b/transport.c
index 272c0f4046..b0451c7a3c 100644
--- a/transport.c
+++ b/transport.c
@@ -463,11 +463,30 @@ static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	char *from_name = NULL;
+	char *to_name = NULL;
+	const char *val;
+	int len;
+
+	if (from && from->remote_status) {
+		val = parse_feature_value(from->remote_status, "ref", &len);
+		if (val && len)
+			 from_name = xmemdupz(val, len);
+	}
+	if (to && to->remote_status) {
+		val = parse_feature_value(to->remote_status, "ref", &len);
+		if (val && len)
+			 to_name = xmemdupz(val, len);
+	}
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag,
+				from_name ? from_name : from->name,
+				to_name ? to_name: to->name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag,
+				to_name ? to_name: to->name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +500,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from_name ? from_name : from->name),
+				prettify_refname(to_name ? to_name : to->name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name ? to_name : to->name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -491,6 +512,10 @@ static void print_ref_status(char flag, const char *summary,
 		}
 		fputc('\n', stderr);
 	}
+	if (from_name)
+		free(from_name);
+	if (to_name)
+		free(to_name);
 }
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v11 5/7] refs.c: refactor to reuse ref_is_hidden()
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
                               ` (4 preceding siblings ...)
  2020-04-13 16:48             ` [PATCH v11 4/7] send-pack: extension for client-side status report Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 13 +++++++++----
 refs.h |  1 +
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..a20f56a463 100644
--- a/refs.c
+++ b/refs.c
@@ -1388,14 +1388,14 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 	return 0;
 }
 
-int ref_is_hidden(const char *refname, const char *refname_full)
+int ref_matches(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
@@ -1421,6 +1421,11 @@ int ref_is_hidden(const char *refname, const char *refname_full)
 	return 0;
 }
 
+int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_matches(hide_refs, refname, refname_full);
+}
+
 const char *find_descendant_ref(const char *dirname,
 				const struct string_list *extras,
 				const struct string_list *skip)
diff --git a/refs.h b/refs.h
index 545029c6d8..d02c72d3ad 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_matches(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v11 6/7] receive-pack: new config receive.procReceiveRefs
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
                               ` (5 preceding siblings ...)
  2020-04-13 16:48             ` [PATCH v11 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  2020-04-13 16:48             ` [PATCH v11 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings match the reference names of the commands
which are sent from git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt | 14 +++++
 builtin/receive-pack.c           | 43 ++++++++++++---
 t/t5411/common-test-cases.sh     | 94 +++++++++++++++++++++++++++++---
 3 files changed, 136 insertions(+), 15 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..e7b967feeb 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running the hook "proc-receive".
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index fd2dc23c2e..7f39f8db9a 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_append(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1750,15 +1765,26 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2223,6 +2249,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2338,5 +2366,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 7fa7211ce4..ff73549a8a 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -53,6 +53,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -607,7 +615,7 @@ test_expect_success "setup proc-receive hook (with extended status)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
 	test-tool proc-receive -v \
-		-r "$ZERO_OID $A refs/for/a/b/c/topic ok" \
+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
 		-r "$ZERO_OID $A refs/for/next/topic ok\0ref=refs/pull/123/head" \
 		-r "$B $A refs/for/master/topic ok\0ref=refs/pull/124/head forced-update"
 	EOF
@@ -615,33 +623,33 @@ test_expect_success "setup proc-receive hook (with extended status)" '
 
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
-# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
 test_expect_success "proc-receive: report with extended status" '
 	git -C workbench push origin \
 		HEAD:refs/for/next/topic \
-		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/review/a/b/c/topic \
 		HEAD:refs/for/master/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
-	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: # proc-receive hook
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
-	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
-	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic ok
+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/123/head
 	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok\0ref=refs/pull/124/head forced-update
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
-	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
 	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
@@ -714,3 +722,73 @@ test_expect_success "proc-receive: report update of mixed refs" '
 	EOF
 	test_cmp expect actual
 '
+
+test_expect_success "config add receive.procReceiveRefs = refs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "$B $A refs/heads/master ft" \
+		-r "$A $ZERO_OID refs/heads/foo ft" \
+		-r "$A $B refs/heads/bar ft" \
+		-r "$A $B refs/for/master/topic ok\0ref=refs/pull/123/head" \
+		-r "$B $A refs/for/next/topic ok\0ref=refs/pull/124/head forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok\0ref=refs/pull/123/head
+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/124/head forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v11 7/7] doc: add documentation for the proc-receive hook
  2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
                               ` (6 preceding siblings ...)
  2020-04-13 16:48             ` [PATCH v11 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-13 16:48             ` Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-13 16:48 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 58 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..1c528e2fc4 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,64 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
+    # NO, I reject it.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status are given in key=value pairs after the null
+    # character.
+    H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
+                forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook may return
+different OID and different reference name as extended status of the
+report line.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v10 2/8] receive-pack: add new proc-receive hook
  2020-04-13 10:58               ` Jiang Xin
@ 2020-04-13 21:50                 ` Junio C Hamano
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                     ` (7 more replies)
  0 siblings, 8 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-04-13 21:50 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> Before making a decision, we must consider some limitations or backward
> compatibility issues.  See the limitations from following code snippet
> of "send-pack.c" and "transport.c":
> ...
> The sideband message stream (stderr of 'proc-receive') will be sent
> directly to the client.  If the client is a very old version of git,
> is is still safe?

These made your assumptions clear to me, I think.

I was expecting that the protocol was purely between receive-pack
and the hook and the communication back to the "push" side was done
by receive-pack, AFTER receive-pack finished talking with the hook,
reading and understanding what the hook did.  And with that
expectation, the protocol to the hook is free of "compatibility"
concern and does not have to be constrained to "one report packet,
which may have to carry multiple updates if the hook updates
multiple refs in response to a single update request packet".

But if you are letting the hook talk directly to the "push" side,
without "receive-pack" even snooping/understanding what is going on,
sure, I can understand why you have to put such a limitation to the
protocol to the hook or use/unuse of the side-band.

I actually think we would need some update (new capability, perhaps)
to the protocol between "push" and "receive-pack" when we want to
fully support things like "you tried to push commit A to
'refs/for/X'; 'refs/for/X' is not created nor updated, but instead
'refs/heads/X' has been updated to commit B, which got created using
info in A".  A partial support can be done by just pretending that
the proposed update for 'refs/for/X' succeeded and nothing else
happened, and that may be a perfectly usable initial version.  But I
would suspect that we eventually would want to be able to tell the
"git push" a bit more, and at that point, we'd probably want new
capabilities between "push" and "receive-pack".

I think we should retain fairly strict control over the vocabulary
used by "receive-pack" to explain what happened on "the server side"
to the client, to avoid fracturing the ecosystem.  With "hook's
output is sent pass-thru back to the client" design, you'd allow a
proprietary proc-receive hook to express what it did in a way that
only a matching proprietary variant of "git push" can understand,
which I do not think is a good thing.

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v12 0/7] New proc-receive hook for centralized workflow
  2020-04-13 21:50                 ` Junio C Hamano
@ 2020-04-14 12:32                   ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 0/8] " Jiang Xin
                                       ` (8 more replies)
  2020-04-14 12:32                   ` [PATCH v12 1/7] transport: not report a non-head push as a branch Jiang Xin
                                     ` (6 subsequent siblings)
  7 siblings, 9 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v11

* Change the syntax of status report from "proc-receive" to "receive-pack".

        # Receive result from the hook.
        # OK, run this command successfully.
        H: PKT-LINE(ok <ref>)
        # NO, I reject it.
        H: PKT-LINE(ng <ref> <reason>)
        # Fall through, let 'receive-pack' to execute it.
        H: PKT-LINE(ft <ref>)
        # OK, but has an alternate reference.  The alternate reference name
        # is given in the third parameter, and other status can be given in
        # key-value pairs.
        H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
                    forced-update)
        H: ... ...
        H: flush-pkt

* Support multiple responses from "proc-receive" for one reference.
  Add test case:

        test_expect_success "setup proc-receive hook (multiple responses)" '
        	write_script "$upstream/hooks/proc-receive" <<-EOF
        	printf >&2 "# proc-receive hook\n"
        	test-tool proc-receive -v \
        		-r "alt refs/for/master/topic refs/changes/23/123/1" \
        		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
        	EOF
        '
        
        # Refs of upstream : master(A)
        # Refs of workbench: master(A)  tags/v123
        # git push         :                       refs/for/master/topic(A)
        test_expect_success "proc-receive: report multiple response" '
        	git -C workbench push origin \
        		HEAD:refs/for/master/topic \
        		>out 2>&1 &&
        	make_user_friendly_and_stable_output <out >actual &&
        	cat >expect <<-EOF &&
        	remote: # pre-receive hook
        	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
        	remote: # proc-receive hook
        	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
        	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
        	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
        	remote: # post-receive hook
        	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
        	To <URL/of/upstream.git>
        	 * [new reference] HEAD -> refs/changes/23/123/1
        	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
        	EOF
        	test_cmp expect actual &&
        	git -C "$upstream" show-ref >out &&
        	make_user_friendly_and_stable_output <out >actual &&
        	cat >expect <<-EOF &&
        	<COMMIT-A> refs/heads/master
        	EOF
        	test_cmp expect actual
        '

* "receive-pack" checks format of the report from "proc-receive", will
  discard unknown status.

* Apply the `SQUASH???` commit.


## Range-diff v11..v12

1:  bef4360e58 ! 1:  b76900cb0f receive-pack: add new proc-receive hook
    @@ Commit message
     
             # Receive result from the hook.
             # OK, run this command successfully.
    -        H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
    +        H: PKT-LINE(ok <ref>)
             # NO, I reject it.
    -        H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
    +        H: PKT-LINE(ng <ref> <reason>)
             # Fall through, let 'receive-pack' to execute it.
    -        H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
    +        H: PKT-LINE(ft <ref>)
             # OK, but has an alternate reference.  The alternate reference name
    -        # and other status are given in key=value pairs after the null
    -        # character.
    -        H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
    +        # is given in the third parameter, and other status can be given in
    +        # key-value pairs.
    +        H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
                         forced-update)
             H: ... ...
             H: flush-pkt
    @@ Commit message
         other routines.  Finally, the result of the execution of these commands
         will be reported to end user.
     
    -    The reporting function from "receive-pack" to "send-pack" is also
    -    extended using a backward compatible way by adding key-value pairs
    -    after an null character, like:
    +    The reporting function from "receive-pack" to "send-pack" is extended
    +    using a backward compatible way by adding key-value pairs after an null
    +    character, like:
     
             # OK, run this command successfully with optional extended-status.
             ok <reference>\0ref=refs/pull/123/head old-oid=...
    @@ Makefile: TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
      TEST_BUILTINS_OBJS += test-read-cache.o
     
      ## builtin/receive-pack.c ##
    +@@
    + #include "remote.h"
    + #include "connect.h"
    + #include "string-list.h"
    ++#include "string.h"
    + #include "sha1-array.h"
    + #include "connected.h"
    + #include "argv-array.h"
     @@ builtin/receive-pack.c: static void write_head_info(void)
      	packet_flush(1);
      }
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +	for (;;) {
     +		struct object_id old_oid, new_oid;
    -+		const char *refname;
    -+		const char *p;
     +		char *status;
    -+		char *msg = NULL;
    -+		int len;
    ++		char *refname;
    ++		char *remains = NULL;
     +
     +		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
     +			break;
    -+		len = strlen(reader->line);
    -+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
    -+		    *p++ != ' ' ||
    -+		    parse_oid_hex(p, &new_oid, &p) ||
    -+		    *p++ != ' ') {
    -+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
    -+				    reader->line);
    -+			code = -1;
    -+			continue;
    ++		status = (char *)reader->line;
    ++		refname = strchr(status, ' ');
    ++		if (refname) {
    ++			*refname++ = '\0';
    ++			remains = strchr(refname, ' ');
    ++			if (remains)
    ++				*remains++ = '\0';
     +		}
    -+
    -+		refname = p;
    -+		status = strchr(p, ' ');
    -+		if (!status) {
    -+			strbuf_addf(errmsg, "proc-receive expected 'old new ref status [msg]', got '%s'\n",
    ++		if (!refname || strlen(refname) == 0) {
    ++			strbuf_addf(errmsg, "proc-receive expected '<status> <ref> ...', got '%s'\n",
     +				    reader->line);
     +			code = -1;
     +			continue;
     +		}
    -+		*status++ = '\0';
    -+		msg = strchr(status, ' ');
    -+		if (msg)
    -+			*msg++ = '\0';
     +
     +		/* first try searching at our hint, falling back to all refs */
     +		if (hint)
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			continue;
     +		}
     +		if (!strcmp(status, "ng")) {
    -+			if (msg)
    -+				hint->error_string = xstrdup(msg);
    ++			if (remains)
    ++				hint->error_string = xstrdup(remains);
     +			else
     +				hint->error_string = "failed";
     +			code = -1;
     +		} else if (!strcmp("ft", status)) {
     +			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
     +			hint->run_proc_receive = 0;
    ++		} else if (!strcmp("alt", status)) {
    ++			char *alt_ref;
    ++
    ++			alt_ref = remains;
    ++			remains = strchr(alt_ref, ' ');
    ++			if (remains)
    ++				*remains++ = '\0';
    ++
    ++			if (hint->extended_status)
    ++				strbuf_addstr(&extended_status, hint->extended_status);
    ++
    ++			strbuf_addf(&extended_status, "%sref=%s",
    ++				    extended_status.len > 0 ? " ": "",
    ++				    alt_ref);
    ++
    ++			if (remains) {
    ++				const char *val;
    ++				int len;
    ++
    ++				val = parse_feature_value(remains, "old-oid", &len);
    ++				if (val && len > 0)
    ++					if (!parse_oid_hex(val, &old_oid, &val))
    ++						strbuf_addf(&extended_status, "%sold-oid=%s",
    ++							   extended_status.len > 0 ? " ": "",
    ++							   oid_to_hex(&old_oid));
    ++
    ++				val = parse_feature_value(remains, "new-oid", &len);
    ++				if (val && len > 0)
    ++					if (!parse_oid_hex(val, &new_oid, &val))
    ++						strbuf_addf(&extended_status, "%snew-oid=%s",
    ++							   extended_status.len > 0 ? " ": "",
    ++							   oid_to_hex(&new_oid));
    ++
    ++				if (parse_feature_request(remains, "forced-update"))
    ++					strbuf_addf(&extended_status, "%sforced-update",
    ++						    extended_status.len > 0 ? " ": "");
    ++			}
    ++
    ++			if (extended_status.len > 0) {
    ++				free((void *)hint->extended_status);
    ++				hint->extended_status = strbuf_detach(&extended_status, NULL);
    ++			}
     +		} else if (strcmp("ok", status)) {
     +			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
     +				    status, refname);
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
     +			continue;
     +		}
    -+		if (reader->pktlen > len)
    -+			strbuf_addstr(&extended_status, (char *)reader->line + len + 1);
    -+		if (oidcmp(&hint->old_oid, &old_oid)) {
    -+			oidcpy(&hint->old_oid, &old_oid);
    -+			strbuf_addf(&extended_status, "%sold-oid=%s",
    -+				    extended_status.len > 0 ? " ": "",
    -+				    oid_to_hex(&old_oid));
    -+		}
    -+		if (oidcmp(&hint->new_oid, &new_oid)) {
    -+			oidcpy(&hint->new_oid, &new_oid);
    -+			strbuf_addf(&extended_status, "%snew-oid=%s",
    -+				    extended_status.len > 0 ? " ": "",
    -+				    oid_to_hex(&new_oid));
    -+		}
    -+		if (extended_status.len > 0)
    -+			hint->extended_status = strbuf_detach(&extended_status, NULL);
    ++
     +		if (hint->run_proc_receive)
     +			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
     +	}
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +
     +'
     +
    -+test_expect_success "setup proc-receive hook (bad oid)" '
    ++test_expect_success "setup proc-receive hook (no ref)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "bad-id new-id ref ok"
    ++		-r "ok"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: bad protocol (bad oid)" '
    ++test_expect_success "proc-receive: bad protocol (no ref)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic\
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> bad-id new-id ref ok
    -+	remote: error: proc-receive expected "old new ref status [msg]", got "bad-id new-id ref ok"
    -+	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
    -+	EOF
    -+	test_cmp expect actual &&
    -+	git -C "$upstream" show-ref >out &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	<COMMIT-A> refs/heads/master
    -+	EOF
    -+	test_cmp expect actual
    -+'
    -+
    -+test_expect_success "setup proc-receive hook (no status)" '
    -+	write_script "$upstream/hooks/proc-receive" <<-EOF
    -+	printf >&2 "# proc-receive hook\n"
    -+	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/master/topic"
    -+	EOF
    -+'
    -+
    -+# Refs of upstream : master(A)
    -+# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: bad protocol (no status)" '
    -+	test_must_fail git -C workbench push origin \
    -+		HEAD:refs/for/master/topic \
    -+		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: error: proc-receive expected "old new ref status [msg]", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	remote: proc-receive> ok
    ++	remote: error: proc-receive expected "<status> <ref> ...", got "ok"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/master/topic xx msg"
    ++		-r "xx refs/for/master/topic"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic xx msg
    ++	remote: proc-receive> xx refs/for/master/topic
     +	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/master/topic ng"
    ++		-r "ng refs/for/master/topic"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng
    ++	remote: proc-receive> ng refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/master/topic ng error msg"
    ++		-r "ng refs/for/master/topic error msg"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ng error msg
    ++	remote: proc-receive> ng refs/for/master/topic error msg
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/heads/master ok"
    ++		-r "ok refs/heads/master"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/heads/master ok
    ++	remote: proc-receive> ok refs/heads/master
     +	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/master/topic ok"
    ++		-r "ok refs/for/master/topic"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    ++	remote: proc-receive> ok refs/for/master/topic
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    ++	remote: proc-receive> ok refs/for/master/topic
     +	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive< issue=123
     +	remote: proc-receive< reviewer=user1
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/master/topic ok
    ++	remote: proc-receive> ok refs/for/master/topic
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $B refs/for/master/topic ft"
    ++		-r "ft refs/for/master/topic"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-B> refs/for/master/topic ft
    ++	remote: proc-receive> ft refs/for/master/topic
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/a/b/c/topic ok" \
    -+		-r "$ZERO_OID $A refs/for/next/topic ok\0ref=refs/pull/123/head" \
    -+		-r "$B $A refs/for/master/topic ok\0ref=refs/pull/124/head forced-update"
    ++		-r "ok refs/for/a/b/c/topic" \
    ++		-r "alt refs/for/next/topic refs/pull/123/head" \
    ++		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/123/head
    -+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok\0ref=refs/pull/124/head forced-update
    ++	remote: proc-receive> ok refs/for/a/b/c/topic
    ++	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    -+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 * [new reference] HEAD -> refs/for/next/topic
     +	 * [new reference] HEAD -> refs/for/a/b/c/topic
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    ++test_expect_success "setup proc-receive hook (multiple responses)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/changes/23/123/1" \
    ++		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report multiple response" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
     +test_expect_success "setup proc-receive hook" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$ZERO_OID $A refs/for/next/topic ok" \
    -+		-r "$A $B refs/for/master/topic ok"
    ++		-r "ok refs/for/next/topic" \
    ++		-r "alt refs/for/master/topic refs/for/master/topic old-oid=$A new-oid=$B"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok
    -+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok
    ++	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    -+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 <OID-A>..<OID-B> <COMMIT-B> -> master
     +	 * [new branch] HEAD -> bar
2:  a4a1cc1d14 ! 2:  d20702291b send-pack: extension for client-side status report
    @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref
      			ret = -1;
      		}
     -		hint->remote_status = xstrdup_or_null(msg);
    -+		if (msg) {
    ++		if (msg)
     +			hint->remote_status = xstrdup(msg);
    -+		} else if (extended_status) {
    ++		else if (extended_status)
     +			hint->remote_status = xstrdup(extended_status);
    -+			update_ref_from_remote_status(hint);
    -+		}
      		/* start our next search from the next ref */
      		hint = hint->next;
      	}
    @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref
      ## t/t5411/common-test-cases.sh ##
     @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report with extended status" '
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    - 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
      	To <URL/of/upstream.git>
     -	 * [new reference] HEAD -> refs/for/next/topic
     +	 * [new reference] HEAD -> refs/pull/123/head
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report with ext
      	EOF
      	test_cmp expect actual &&
      	git -C "$upstream" show-ref >out &&
    +@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report multiple response" '
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/changes/23/123/1
    ++	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
    + 	EOF
    + 	test_cmp expect actual &&
    + 	git -C "$upstream" show-ref >out &&
     @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update of mixed refs" '
      	 * [new branch] HEAD -> baz
      	 * [new reference] HEAD -> refs/for/next/topic
    @@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
      		}
      	}
      
    -@@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
    - 	(*ref)->status = status;
    - 	(*ref)->forced_update |= forced;
    - 	(*ref)->remote_status = msg;
    -+	if (status == REF_STATUS_OK && (*ref)->remote_status)
    -+		update_ref_from_remote_status(*ref);
    - 	return !(status == REF_STATUS_OK);
    - }
    - 
     
      ## transport.c ##
     @@ transport.c: static void print_ref_status(char flag, const char *summary,
      			     struct ref *to, struct ref *from, const char *msg,
      			     int porcelain, int summary_width)
      {
    -+	char *from_name = NULL;
     +	char *to_name = NULL;
     +	const char *val;
     +	int len;
     +
    -+	if (from && from->remote_status) {
    -+		val = parse_feature_value(from->remote_status, "ref", &len);
    -+		if (val && len)
    -+			 from_name = xmemdupz(val, len);
    -+	}
     +	if (to && to->remote_status) {
     +		val = parse_feature_value(to->remote_status, "ref", &len);
     +		if (val && len)
    -+			 to_name = xmemdupz(val, len);
    ++		to_name = xmemdupz(val, len);
     +	}
     +
      	if (porcelain) {
      		if (from)
     -			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
    -+			fprintf(stdout, "%c\t%s:%s\t", flag,
    -+				from_name ? from_name : from->name,
    -+				to_name ? to_name: to->name);
    ++			fprintf(stdout, "%c\t%s:%s\t", flag, from->name,
    ++				to_name ? to_name : to->name);
      		else
     -			fprintf(stdout, "%c\t:%s\t", flag, to->name);
     +			fprintf(stdout, "%c\t:%s\t", flag,
    -+				to_name ? to_name: to->name);
    ++				to_name ? to_name : to->name);
      		if (msg)
      			fprintf(stdout, "%s (%s)\n", summary, msg);
      		else
    @@ transport.c: static void print_ref_status(char flag, const char *summary,
      		if (from)
     -			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
     +			fprintf(stderr, "%s -> %s",
    -+				prettify_refname(from_name ? from_name : from->name),
    ++				prettify_refname(from->name),
     +				prettify_refname(to_name ? to_name : to->name));
      		else
     -			fputs(prettify_refname(to->name), stderr);
    @@ transport.c: static void print_ref_status(char flag, const char *summary,
      		}
      		fputc('\n', stderr);
      	}
    -+	if (from_name)
    -+		free(from_name);
    -+	if (to_name)
    -+		free(to_name);
    ++	free(to_name);
      }
      
      static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
    +@@ transport.c: static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
    + 	}
    + }
    + 
    +-static int print_one_push_status(struct ref *ref, const char *dest, int count,
    +-				 int porcelain, int summary_width)
    ++static int _print_one_push_status(struct ref *ref, const char *dest, int count,
    ++				  int porcelain, int summary_width)
    + {
    + 	if (!count) {
    + 		char *url = transport_anonymize_url(dest);
    +@@ transport.c: static int print_one_push_status(struct ref *ref, const char *dest, int count,
    + 	return 1;
    + }
    + 
    ++static int print_one_push_status(struct ref *ref, const char *dest, int count,
    ++				 int porcelain, int summary_width)
    ++{
    ++	char *head;
    ++	char *begin;
    ++	int n = 0;
    ++
    ++	if (!ref->remote_status)
    ++		return _print_one_push_status(ref, dest, count,
    ++					      porcelain, summary_width);
    ++
    ++	head = ref->remote_status;
    ++	begin = strstr(head, "ref=");
    ++	if (!begin)
    ++		begin = ref->remote_status;
    ++	for (;;) {
    ++		char *end;
    ++		struct object_id old_oid;
    ++		struct object_id new_oid;
    ++		int forced_update;
    ++
    ++		end = strstr(begin + 4, "ref=");
    ++		if (end)
    ++			*(end-1) = '\0';
    ++
    ++		oidcpy(&old_oid, &ref->old_oid);
    ++		oidcpy(&new_oid, &ref->new_oid);
    ++		forced_update = ref->forced_update;
    ++
    ++		ref->remote_status = begin;
    ++		update_ref_from_remote_status(ref);
    ++		_print_one_push_status(ref, dest, count + n++,
    ++				       porcelain, summary_width);
    ++
    ++		oidcpy(&ref->old_oid, &old_oid);
    ++		oidcpy(&ref->new_oid, &new_oid);
    ++		ref->forced_update = forced_update;
    ++
    ++		if (end) {
    ++			begin = end;
    ++			*(end-1) = ' ';
    ++		} else {
    ++			break;
    ++		}
    ++	}
    ++	ref->remote_status = head;
    ++	return n;
    ++}
    ++
    + static int measure_abbrev(const struct object_id *oid, int sofar)
    + {
    + 	char hex[GIT_MAX_HEXSZ + 1];
3:  4e3c383c12 = 3:  a8860faa17 refs.c: refactor to reuse ref_is_hidden()
4:  dd83569c4e ! 4:  46a839ddf6 receive-pack: new config receive.procReceiveRefs
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +	if (proc_receive_refs.nr > 0) {
     +		struct strbuf refname_full = STRBUF_INIT;
     +		size_t prefix_len;
    ++
    ++		strbuf_addstr(&refname_full, get_git_namespace());
    ++		prefix_len = refname_full.len;
      
     -		/* TODO: replace the fixed prefix by looking up git config variables. */
     -		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
     -			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     -			run_proc_receive = 1;
    -+		strbuf_addstr(&refname_full, get_git_namespace());
    -+		prefix_len = refname_full.len;
    -+
     +		for (cmd = commands; cmd; cmd = cmd->next) {
     +			if (!should_process_cmd(cmd))
     +				continue;
    @@ t/t5411/common-test-cases.sh: test_expect_success "setup proc-receive hook (with
      	write_script "$upstream/hooks/proc-receive" <<-EOF
      	printf >&2 "# proc-receive hook\n"
      	test-tool proc-receive -v \
    --		-r "$ZERO_OID $A refs/for/a/b/c/topic ok" \
    -+		-r "$ZERO_OID $A refs/review/a/b/c/topic ok" \
    - 		-r "$ZERO_OID $A refs/for/next/topic ok\0ref=refs/pull/123/head" \
    - 		-r "$B $A refs/for/master/topic ok\0ref=refs/pull/124/head forced-update"
    +-		-r "ok refs/for/a/b/c/topic" \
    ++		-r "ok refs/review/a/b/c/topic" \
    + 		-r "alt refs/for/next/topic refs/pull/123/head" \
    + 		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
      	EOF
     @@ t/t5411/common-test-cases.sh: test_expect_success "setup proc-receive hook (with extended status)" '
      
    @@ t/t5411/common-test-cases.sh: test_expect_success "setup proc-receive hook (with
     -	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
      	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    --	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic ok
    -+	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic ok
    - 	remote: proc-receive> <ZERO-OID> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/123/head
    - 	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/master/topic ok\0ref=refs/pull/124/head forced-update
    +-	remote: proc-receive> ok refs/for/a/b/c/topic
    ++	remote: proc-receive> ok refs/review/a/b/c/topic
    + 	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    + 	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
      	remote: # post-receive hook
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     -	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    - 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
      	To <URL/of/upstream.git>
      	 * [new reference] HEAD -> refs/pull/123/head
     -	 * [new reference] HEAD -> refs/for/a/b/c/topic
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update o
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "$B $A refs/heads/master ft" \
    -+		-r "$A $ZERO_OID refs/heads/foo ft" \
    -+		-r "$A $B refs/heads/bar ft" \
    -+		-r "$A $B refs/for/master/topic ok\0ref=refs/pull/123/head" \
    -+		-r "$B $A refs/for/next/topic ok\0ref=refs/pull/124/head forced-update"
    ++		-r "ft refs/heads/master" \
    ++		-r "ft refs/heads/foo" \
    ++		-r "ft refs/heads/bar" \
    ++		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$A new-oid=$B " \
    ++		-r "alt refs/for/next/topic refs/pull/124/head old-oid=$B new-oid=$A forced-update"
     +	EOF
     +'
     +
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update o
     +	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/heads/master ft
    -+	remote: proc-receive> <COMMIT-A> <ZERO-OID> refs/heads/foo ft
    -+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/heads/bar ft
    -+	remote: proc-receive> <COMMIT-A> <COMMIT-B> refs/for/master/topic ok\0ref=refs/pull/123/head
    -+	remote: proc-receive> <COMMIT-B> <COMMIT-A> refs/for/next/topic ok\0ref=refs/pull/124/head forced-update
    ++	remote: proc-receive> ft refs/heads/master
    ++	remote: proc-receive> ft refs/heads/foo
    ++	remote: proc-receive> ft refs/heads/bar
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> alt refs/for/next/topic refs/pull/124/head old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
     +	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
     +	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
    -+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
    -+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	To <URL/of/upstream.git>
     +	 <OID-A>..<OID-B> <COMMIT-B> -> bar
     +	 - [deleted] foo
5:  2d04f495a3 ! 5:  a357234e97 doc: add documentation for the proc-receive hook
    @@ Documentation/githooks.txt: The default 'update' hook, when enabled--and with
     +
     +    # Receive result from the hook.
     +    # OK, run this command successfully.
    -+    H: PKT-LINE(<old-oid> <new-oid> <ref> ok)
    ++    H: PKT-LINE(ok <ref>)
     +    # NO, I reject it.
    -+    H: PKT-LINE(<old-oid> <new-oid> <ref> ng <reason>)
    ++    H: PKT-LINE(ng <ref> <reason>)
     +    # Fall through, let 'receive-pack' to execute it.
    -+    H: PKT-LINE(<old-oid> <new-oid> <ref> ft)
    ++    H: PKT-LINE(ft <ref>)
     +    # OK, but has an alternate reference.  The alternate reference name
    -+    # and other status are given in key=value pairs after the null
    -+    # character.
    -+    H: PKT-LINE(<old-oid> <new-oid> <ref> ok\0ref=refs/pull/123/head
    ++    # is given in the third parameter, and other status can be given in
    ++    # key-value pairs.
    ++    H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
     +                forced-update)
     +    H: ... ...
     +    H: flush-pkt

---

Jiang Xin (7):
  transport: not report a non-head push as a branch
  connect: export parse_feature_value()
  receive-pack: add new proc-receive hook
  send-pack: extension for client-side status report
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt           |  14 +
 Documentation/githooks.txt                 |  58 ++
 Makefile                                   |   1 +
 builtin/receive-pack.c                     | 345 ++++++++-
 connect.c                                  |   3 +-
 connect.h                                  |   1 +
 refs.c                                     |  13 +-
 refs.h                                     |   1 +
 remote.c                                   |  28 +
 remote.h                                   |   3 +
 send-pack.c                                |  11 +-
 t/helper/test-proc-receive.c               | 178 +++++
 t/helper/test-tool.c                       |   1 +
 t/helper/test-tool.h                       |   1 +
 t/t5411-proc-receive-hook.sh               |  67 ++
 t/t5411/common-functions.sh                |  54 ++
 t/t5411/common-test-cases.sh               | 798 +++++++++++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh |  78 ++
 t/t5516-fetch-push.sh                      |   2 +-
 transport-helper.c                         |  64 +-
 transport.c                                |  85 ++-
 21 files changed, 1749 insertions(+), 57 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v12 1/7] transport: not report a non-head push as a branch
  2020-04-13 21:50                 ` Junio C Hamano
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-04-14 12:32                   ` Jiang Xin
  2020-04-14 12:32                   ` [PATCH v12 2/7] connect: export parse_feature_value() Jiang Xin
                                     ` (5 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh               | 67 +++++++++++++++++++
 t/t5411/common-functions.sh                | 50 ++++++++++++++
 t/t5411/common-test-cases.sh               | 43 ++++++++++++
 t/t5412-proc-receive-hook-http-protocol.sh | 78 ++++++++++++++++++++++
 t/t5516-fetch-push.sh                      |  2 +-
 transport.c                                |  9 ++-
 6 files changed, 245 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/common-test-cases.sh
 create mode 100755 t/t5412-proc-receive-hook-http-protocol.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..3f4bee76d9
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace the URL of different protocol for the upstream repository
+# with a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common |
+		sed -e "s#To ../upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	write_script upstream.git/hooks/pre-receive <<-\EOF &&
+	exec >&2
+	echo "# pre-receive hook"
+	while read old new ref
+	do
+		echo "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	write_script upstream.git/hooks/post-receive <<-\EOF &&
+	exec >&2
+	echo "# post-receive hook"
+	while read old new ref
+	do
+		echo "post-receive< $old $new $ref"
+	done
+	EOF
+
+	upstream=upstream.git
+'
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..4337b10c8c
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,50 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  We also replce single quotes with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.
+make_user_friendly_and_stable_output_common () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
+}
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
new file mode 100644
index 0000000000..23655846e4
--- /dev/null
+++ b/t/t5411/common-test-cases.sh
@@ -0,0 +1,43 @@
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "normal git-push command" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5412-proc-receive-hook-http-protocol.sh b/t/t5412-proc-receive-hook-http-protocol.sh
new file mode 100755
index 0000000000..d102bc438b
--- /dev/null
+++ b/t/t5412-proc-receive-hook-http-protocol.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook for HTTP protocol'
+
+. ./test-lib.sh
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  In addition to the common format method,
+# we also replace the URL of different protocol for the upstream repository
+# with a fixed pattern.
+make_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output_common |
+		sed -e "s#To http:.*/upstream.git#To <URL/of/upstream.git>#"
+}
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git -C upstream.git config http.receivepack true &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git tag -m "v123" v123 $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/next
+	) &&
+	TAG=$(git -C workbench rev-parse v123) &&
+
+	# setup pre-receive hook
+	write_script upstream.git/hooks/pre-receive <<-\EOF &&
+	exec >&2
+	echo "# pre-receive hook"
+	while read old new ref
+	do
+		echo "pre-receive< $old $new $ref"
+	done
+	EOF
+
+	# setup post-receive hook
+	write_script upstream.git/hooks/post-receive <<-\EOF &&
+	exec >&2
+	echo "# post-receive hook"
+	while read old new ref
+	do
+		echo "post-receive< $old $new $ref"
+	done
+	EOF
+
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Include test cases for both file and HTTP protocol
+. "$TEST_DIRECTORY"/t5411/common-test-cases.sh
+
+test_done
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9e4b9313b5 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..272c0f4046 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v12 2/7] connect: export parse_feature_value()
  2020-04-13 21:50                 ` Junio C Hamano
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
  2020-04-14 12:32                   ` [PATCH v12 1/7] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-14 12:32                   ` Jiang Xin
  2020-04-14 12:32                   ` [PATCH v12 3/7] receive-pack: add new proc-receive hook Jiang Xin
                                     ` (4 subsequent siblings)
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Export `parse_feature_value()` to parse extended status of report for
"proc-receive" and "receive-pack".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 connect.c | 3 +--
 connect.h | 1 +
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/connect.c b/connect.c
index b6451ab5e8..08e0616726 100644
--- a/connect.c
+++ b/connect.c
@@ -18,7 +18,6 @@
 
 static char *server_capabilities_v1;
 static struct argv_array server_capabilities_v2 = ARGV_ARRAY_INIT;
-static const char *parse_feature_value(const char *, const char *, int *);
 
 static int check_ref(const char *name, unsigned int flags)
 {
@@ -447,7 +446,7 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
 	return list;
 }
 
-static const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
+const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
 {
 	int len;
 
diff --git a/connect.h b/connect.h
index 5f2382e018..17b3252cbc 100644
--- a/connect.h
+++ b/connect.h
@@ -12,6 +12,7 @@ int finish_connect(struct child_process *conn);
 int git_connection_is_socket(struct child_process *conn);
 int server_supports(const char *feature);
 int parse_feature_request(const char *features, const char *feature);
+const char *parse_feature_value(const char *feature_list, const char *feature, int *len_ret);
 const char *server_feature_value(const char *feature, int *len_ret);
 int url_is_local_not_ssh(const char *url);
 
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-13 21:50                 ` Junio C Hamano
                                     ` (2 preceding siblings ...)
  2020-04-14 12:32                   ` [PATCH v12 2/7] connect: export parse_feature_value() Jiang Xin
@ 2020-04-14 12:32                   ` Jiang Xin
  2020-04-15 15:48                     ` Junio C Hamano
  2020-04-14 12:32                   ` [PATCH v12 4/7] send-pack: extension for client-side status report Jiang Xin
                                     ` (3 subsequent siblings)
  7 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ft <ref>)
    # OK, but has an alternate reference.  The alternate reference name
    # is given in the third parameter, and other status can be given in
    # key-value pairs.
    H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
                forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in key-value pairs as extended status of the report
line.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" is extended
using a backward compatible way by adding key-value pairs after an null
character, like:

    # OK, run this command successfully with optional extended-status.
    ok <reference>\0ref=refs/pull/123/head old-oid=...

    # NO, I reject it.
    ng <reference> <error message>

Parsing the extended status of the report lines for "send-pack" is not
implemented in this commit, so we can make a compatible test on old
version of a Git client.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                     |   1 +
 builtin/receive-pack.c       | 316 +++++++++++++++-
 t/helper/test-proc-receive.c | 178 +++++++++
 t/helper/test-tool.c         |   1 +
 t/helper/test-tool.h         |   1 +
 t/t5411/common-functions.sh  |  10 +-
 t/t5411/common-test-cases.sh | 676 +++++++++++++++++++++++++++++++++++
 7 files changed, 1171 insertions(+), 12 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..fbbb01135e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -13,6 +13,7 @@
 #include "remote.h"
 #include "connect.h"
 #include "string-list.h"
+#include "string.h"
 #include "sha1-array.h"
 #include "connected.h"
 #include "argv-array.h"
@@ -308,11 +309,15 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extended_status;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +822,266 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	struct strbuf extended_status = STRBUF_INIT;
+	int code = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		char *status;
+		char *refname;
+		char *remains = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		status = (char *)reader->line;
+		refname = strchr(status, ' ');
+		if (refname) {
+			*refname++ = '\0';
+			remains = strchr(refname, ' ');
+			if (remains)
+				*remains++ = '\0';
+		}
+		if (!refname || strlen(refname) == 0) {
+			strbuf_addf(errmsg, "proc-receive expected '<status> <ref> ...', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!strcmp(status, "ng")) {
+			if (remains)
+				hint->error_string = xstrdup(remains);
+			else
+				hint->error_string = "failed";
+			code = -1;
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else if (!strcmp("alt", status)) {
+			char *alt_ref;
+
+			alt_ref = remains;
+			remains = strchr(alt_ref, ' ');
+			if (remains)
+				*remains++ = '\0';
+
+			if (hint->extended_status)
+				strbuf_addstr(&extended_status, hint->extended_status);
+
+			strbuf_addf(&extended_status, "%sref=%s",
+				    extended_status.len > 0 ? " ": "",
+				    alt_ref);
+
+			if (remains) {
+				const char *val;
+				int len;
+
+				val = parse_feature_value(remains, "old-oid", &len);
+				if (val && len > 0)
+					if (!parse_oid_hex(val, &old_oid, &val))
+						strbuf_addf(&extended_status, "%sold-oid=%s",
+							   extended_status.len > 0 ? " ": "",
+							   oid_to_hex(&old_oid));
+
+				val = parse_feature_value(remains, "new-oid", &len);
+				if (val && len > 0)
+					if (!parse_oid_hex(val, &new_oid, &val))
+						strbuf_addf(&extended_status, "%snew-oid=%s",
+							   extended_status.len > 0 ? " ": "",
+							   oid_to_hex(&new_oid));
+
+				if (parse_feature_request(remains, "forced-update"))
+					strbuf_addf(&extended_status, "%sforced-update",
+						    extended_status.len > 0 ? " ": "");
+			}
+
+			if (extended_status.len > 0) {
+				free((void *)hint->extended_status);
+				hint->extended_status = strbuf_detach(&extended_status, NULL);
+			}
+		} else if (strcmp("ok", status)) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    status, refname);
+			code = -1;
+			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
+			continue;
+		}
+
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->error_string &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "proc-receive failed to report status";
+		    code = -1;
+		}
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1657,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1697,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1733,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1763,21 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1804,12 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+				cmd->error_string = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -1899,12 +2186,23 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
-			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+		if (!cmd->error_string) {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, '\0',
+						 cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+		} else {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ng %s %s%c%s\n",
+						 cmd->ref_name, cmd->error_string,
+						 '\0', cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ng %s %s\n",
+						 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..c9f5c5aae4
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,178 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int version = 1;
+static int verbose = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ')
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns) {
+			char *p;
+
+			p = strstr(item->string, "\\0");
+			if (p) {
+				*p = '\0';
+				p += 2;
+				packet_write_fmt(1, "%s%c%s\n", item->string, '\0', p);
+			} else {
+				packet_write_fmt(1, "%s\n", item->string);
+			}
+		}
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 4337b10c8c..1fbd24dbaf 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -34,8 +34,11 @@ create_commits_in () {
 # Format the output of git-push, git-show-ref and other commands to make a
 # user-friendly and stable text.  We can easily prepare the expect text
 # without having to worry about future changes of the commit ID and spaces
-# of the output.  We also replce single quotes with double quotes, because
-# it is boring to prepare unquoted single quotes in expect text.
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
 make_user_friendly_and_stable_output_common () {
 	sed \
 		-e "s/  *\$//" \
@@ -46,5 +49,6 @@ make_user_friendly_and_stable_output_common () {
 		-e "s/$TAG/<TAG-v123>/g" \
 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
-		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "/^error: / d"
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 23655846e4..e850292e50 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -41,3 +41,679 @@ test_expect_success "normal git-push command" '
 	EOF
 	test_cmp expect actual
 '
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail all for atomic push" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown version)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { s/   */ /g; p; }" \
+		-e "/^ / { s/   */ /g; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	sed -n -e "/^remote: error:/ { s/   */ /g; p; }" \
+		<actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive expected "<status> <ref> ...", got "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng message)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unexpected ref)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report on unexpected ref" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
+
+test_expect_success "setup proc-receive hook (ok)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: failed to report status" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+test_expect_success "setup proc-receive hook (ft)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ft refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ft refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
+
+test_expect_success "setup proc-receive hook (with extended status)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "alt refs/for/next/topic refs/pull/123/head" \
+		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with extended status" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
+	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple responses)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/changes/23/123/1" \
+		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report multiple response" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic" \
+		-r "alt refs/for/master/topic refs/for/master/topic old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> alt refs/for/master/topic refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v12 4/7] send-pack: extension for client-side status report
  2020-04-13 21:50                 ` Junio C Hamano
                                     ` (3 preceding siblings ...)
  2020-04-14 12:32                   ` [PATCH v12 3/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-14 12:32                   ` Jiang Xin
  2020-04-15 20:36                     ` Junio C Hamano
  2020-04-14 12:32                   ` [PATCH v12 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                                     ` (2 subsequent siblings)
  7 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This is the client-side extension for status report of git-push.

The "proc-receive" hook may receive a command for a pseudo-reference
with a zero-old as its old-oid, while the result of the hook may point
to an alternate reference and the reference may exist already with a
non-zero old-oid.  Git client may receive a report with extened status
as the following example:

    ok refs/for/master/topic\0ref=refs/pull/123/head old-oid=...

Parse the key-value pairs in the extended status after the null
character of the report, and show the proper reference name, old-oid
and new-oid to the user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 remote.c                     | 28 +++++++++++++
 remote.h                     |  3 ++
 send-pack.c                  | 11 +++++-
 t/t5411/common-test-cases.sh |  9 +++--
 transport-helper.c           | 64 +++++++++++++++---------------
 transport.c                  | 76 +++++++++++++++++++++++++++++++++---
 6 files changed, 149 insertions(+), 42 deletions(-)

diff --git a/remote.c b/remote.c
index c43196ec06..0b22d9e14c 100644
--- a/remote.c
+++ b/remote.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "config.h"
+#include "connect.h"
 #include "remote.h"
 #include "refs.h"
 #include "refspec.h"
@@ -2311,3 +2312,30 @@ void apply_push_cas(struct push_cas_option *cas,
 	for (ref = remote_refs; ref; ref = ref->next)
 		apply_cas(cas, remote, ref);
 }
+
+void update_ref_from_remote_status(struct ref *ref)
+{
+	char *val;
+	char c;
+	int len;
+
+	if (!ref->remote_status)
+		return;
+
+	val = (char *)parse_feature_value(ref->remote_status, "old-oid", &len);
+	if (val && len) {
+		c = *(val+len);
+		*(val+len) = '\0';
+		get_oid_hex(val, &ref->old_oid);
+		*(val+len) = c;
+	}
+	val = (char *)parse_feature_value(ref->remote_status, "new-oid", &len);
+	if (val && len) {
+		c = *(val+len);
+		*(val+len) = '\0';
+		get_oid_hex(val, &ref->new_oid);
+		*(val+len) = c;
+	}
+	if (parse_feature_request(ref->remote_status, "forced-update"))
+		ref->forced_update = 1;
+}
diff --git a/remote.h b/remote.h
index 11d8719b58..09cf7014e3 100644
--- a/remote.h
+++ b/remote.h
@@ -345,4 +345,7 @@ int parseopt_push_cas_option(const struct option *, const char *arg, int unset);
 int is_empty_cas(const struct push_cas_option *);
 void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
 
+/* Parse key-value pairs of remote_status and update the reference accordingly */
+void update_ref_from_remote_status(struct ref *ref);
+
 #endif
diff --git a/send-pack.c b/send-pack.c
index 0407841ae8..166e986aa6 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -159,6 +159,9 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 	while (1) {
 		const char *refname;
 		char *msg;
+		char *extended_status = NULL;
+		int len;
+
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
@@ -167,10 +170,13 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			break;
 		}
 
+		len = strlen(reader->line);
 		refname = reader->line + 3;
 		msg = strchr(refname, ' ');
 		if (msg)
 			*msg++ = '\0';
+		if (reader->pktlen > len)
+			extended_status = (char *)reader->line + len + 1;
 
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
@@ -194,7 +200,10 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			hint->status = REF_STATUS_REMOTE_REJECT;
 			ret = -1;
 		}
-		hint->remote_status = xstrdup_or_null(msg);
+		if (msg)
+			hint->remote_status = xstrdup(msg);
+		else if (extended_status)
+			hint->remote_status = xstrdup(extended_status);
 		/* start our next search from the next ref */
 		hint = hint->next;
 	}
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index e850292e50..397748dd6f 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -605,9 +605,9 @@ test_expect_success "proc-receive: report with extended status" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -645,7 +645,8 @@ test_expect_success "proc-receive: report multiple response" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -704,7 +705,7 @@ test_expect_success "proc-receive: report update of mixed refs" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..cec3495d59 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
diff --git a/transport.c b/transport.c
index 272c0f4046..dbff7c50e5 100644
--- a/transport.c
+++ b/transport.c
@@ -463,11 +463,23 @@ static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	char *to_name = NULL;
+	const char *val;
+	int len;
+
+	if (to && to->remote_status) {
+		val = parse_feature_value(to->remote_status, "ref", &len);
+		if (val && len)
+		to_name = xmemdupz(val, len);
+	}
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name,
+				to_name ? to_name : to->name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag,
+				to_name ? to_name : to->name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +493,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name ? to_name : to->name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name ? to_name : to->name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -491,6 +505,7 @@ static void print_ref_status(char flag, const char *summary,
 		}
 		fputc('\n', stderr);
 	}
+	free(to_name);
 }
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
@@ -531,8 +546,8 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
-				 int porcelain, int summary_width)
+static int _print_one_push_status(struct ref *ref, const char *dest, int count,
+				  int porcelain, int summary_width)
 {
 	if (!count) {
 		char *url = transport_anonymize_url(dest);
@@ -602,6 +617,55 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	char *head;
+	char *begin;
+	int n = 0;
+
+	if (!ref->remote_status)
+		return _print_one_push_status(ref, dest, count,
+					      porcelain, summary_width);
+
+	head = ref->remote_status;
+	begin = strstr(head, "ref=");
+	if (!begin)
+		begin = ref->remote_status;
+	for (;;) {
+		char *end;
+		struct object_id old_oid;
+		struct object_id new_oid;
+		int forced_update;
+
+		end = strstr(begin + 4, "ref=");
+		if (end)
+			*(end-1) = '\0';
+
+		oidcpy(&old_oid, &ref->old_oid);
+		oidcpy(&new_oid, &ref->new_oid);
+		forced_update = ref->forced_update;
+
+		ref->remote_status = begin;
+		update_ref_from_remote_status(ref);
+		_print_one_push_status(ref, dest, count + n++,
+				       porcelain, summary_width);
+
+		oidcpy(&ref->old_oid, &old_oid);
+		oidcpy(&ref->new_oid, &new_oid);
+		ref->forced_update = forced_update;
+
+		if (end) {
+			begin = end;
+			*(end-1) = ' ';
+		} else {
+			break;
+		}
+	}
+	ref->remote_status = head;
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v12 5/7] refs.c: refactor to reuse ref_is_hidden()
  2020-04-13 21:50                 ` Junio C Hamano
                                     ` (4 preceding siblings ...)
  2020-04-14 12:32                   ` [PATCH v12 4/7] send-pack: extension for client-side status report Jiang Xin
@ 2020-04-14 12:32                   ` Jiang Xin
  2020-04-14 12:32                   ` [PATCH v12 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
  2020-04-14 12:32                   ` [PATCH v12 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 13 +++++++++----
 refs.h |  1 +
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..a20f56a463 100644
--- a/refs.c
+++ b/refs.c
@@ -1388,14 +1388,14 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 	return 0;
 }
 
-int ref_is_hidden(const char *refname, const char *refname_full)
+int ref_matches(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
@@ -1421,6 +1421,11 @@ int ref_is_hidden(const char *refname, const char *refname_full)
 	return 0;
 }
 
+int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_matches(hide_refs, refname, refname_full);
+}
+
 const char *find_descendant_ref(const char *dirname,
 				const struct string_list *extras,
 				const struct string_list *skip)
diff --git a/refs.h b/refs.h
index 545029c6d8..d02c72d3ad 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_matches(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v12 6/7] receive-pack: new config receive.procReceiveRefs
  2020-04-13 21:50                 ` Junio C Hamano
                                     ` (5 preceding siblings ...)
  2020-04-14 12:32                   ` [PATCH v12 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-14 12:32                   ` Jiang Xin
  2020-04-14 12:32                   ` [PATCH v12 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings match the reference names of the commands
which are sent from git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt | 14 +++++
 builtin/receive-pack.c           | 43 ++++++++++++---
 t/t5411/common-test-cases.sh     | 94 +++++++++++++++++++++++++++++---
 3 files changed, 136 insertions(+), 15 deletions(-)

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..e7b967feeb 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running the hook "proc-receive".
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index fbbb01135e..40409b6dae 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -77,6 +77,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -229,6 +230,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_append(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1767,15 +1782,26 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2240,6 +2266,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2355,5 +2383,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/common-test-cases.sh b/t/t5411/common-test-cases.sh
index 397748dd6f..aa6319cf4d 100644
--- a/t/t5411/common-test-cases.sh
+++ b/t/t5411/common-test-cases.sh
@@ -53,6 +53,14 @@ test_expect_success "cleanup" '
 	)
 '
 
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
+
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
 # git push         :                       next(A)  refs/for/master/topic(A)
@@ -572,7 +580,7 @@ test_expect_success "setup proc-receive hook (with extended status)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
 	test-tool proc-receive -v \
-		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/review/a/b/c/topic" \
 		-r "alt refs/for/next/topic refs/pull/123/head" \
 		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
 	EOF
@@ -580,33 +588,33 @@ test_expect_success "setup proc-receive hook (with extended status)" '
 
 # Refs of upstream : master(A)
 # Refs of workbench: master(A)  tags/v123
-# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
 test_expect_success "proc-receive: report with extended status" '
 	git -C workbench push origin \
 		HEAD:refs/for/next/topic \
-		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/review/a/b/c/topic \
 		HEAD:refs/for/master/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
-	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: # proc-receive hook
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
-	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
-	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/review/a/b/c/topic
 	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
 	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
-	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/review/a/b/c/topic
 	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
@@ -718,3 +726,73 @@ test_expect_success "proc-receive: report update of mixed refs" '
 	EOF
 	test_cmp expect actual
 '
+
+test_expect_success "config add receive.procReceiveRefs = refs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ft refs/heads/master" \
+		-r "ft refs/heads/foo" \
+		-r "ft refs/heads/bar" \
+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$A new-oid=$B " \
+		-r "alt refs/for/next/topic refs/pull/124/head old-oid=$B new-oid=$A forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ft refs/heads/master
+	remote: proc-receive> ft refs/heads/foo
+	remote: proc-receive> ft refs/heads/bar
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: proc-receive> alt refs/for/next/topic refs/pull/124/head old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v12 7/7] doc: add documentation for the proc-receive hook
  2020-04-13 21:50                 ` Junio C Hamano
                                     ` (6 preceding siblings ...)
  2020-04-14 12:32                   ` [PATCH v12 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-14 12:32                   ` Jiang Xin
  7 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-14 12:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 58 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..a0ea829435 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,64 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ft <ref>)
+    # OK, but has an alternate reference.  The alternate reference name
+    # is given in the third parameter, and other status can be given in
+    # key-value pairs.
+    H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
+                forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook may return
+different OID and different reference name as extended status of the
+report line.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-14 12:32                   ` [PATCH v12 3/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-15 15:48                     ` Junio C Hamano
  2020-04-15 15:55                       ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-04-15 15:48 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index 2cc18bbffd..fbbb01135e 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -13,6 +13,7 @@
>  #include "remote.h"
>  #include "connect.h"
>  #include "string-list.h"
> +#include "string.h"
>  #include "sha1-array.h"
>  #include "connected.h"
>  #include "argv-array.h"

What are you using from the system <string.h>, that is not covered
by existing inclusion of "git-compat-util.h"?  

Don't include any system header directly in our *.c code (unless it
is a platform-specific compat/ thing).

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-15 15:48                     ` Junio C Hamano
@ 2020-04-15 15:55                       ` Jiang Xin
  2020-04-15 18:34                         ` Junio C Hamano
  0 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-15 15:55 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年4月15日周三 下午11:48写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> > index 2cc18bbffd..fbbb01135e 100644
> > --- a/builtin/receive-pack.c
> > +++ b/builtin/receive-pack.c
> > @@ -13,6 +13,7 @@
> >  #include "remote.h"
> >  #include "connect.h"
> >  #include "string-list.h"
> > +#include "string.h"
> >  #include "sha1-array.h"
> >  #include "connected.h"
> >  #include "argv-array.h"
>
> What are you using from the system <string.h>, that is not covered
> by existing inclusion of "git-compat-util.h"?
>
> Don't include any system header directly in our *.c code (unless it
> is a platform-specific compat/ thing).

Will correct it.

What I am working on for reroll v13:

* Add new commit "receive-pack: feed extended_status to post-receive“

   When commands are fed to the "post-receive" hook, `extended_status` will
   be parsed and the real old-oid, new-oid, reference name will feed to
   the "post-receive" hook.

* Add test cases for "git push --porcelain".

   I found some changes not covered by test.  So I decide add some
   test cases for "git push --porcelain".  I will split the test cases into
   multiple files inside "t5411/" like:

       t5411/test-0000-normal-push.sh
       t5411/test-0001-normal-push--porcelain.sh

* Other minor fixes.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-15 15:55                       ` Jiang Xin
@ 2020-04-15 18:34                         ` Junio C Hamano
  2020-04-27 17:00                           ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-04-15 18:34 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> Will correct it.
>
> What I am working on for reroll v13:
>
> * Add new commit "receive-pack: feed extended_status to post-receive“
>
>    When commands are fed to the "post-receive" hook, `extended_status` will
>    be parsed and the real old-oid, new-oid, reference name will feed to
>    the "post-receive" hook.
>
> * Add test cases for "git push --porcelain".
>
>    I found some changes not covered by test.  So I decide add some
>    test cases for "git push --porcelain".  I will split the test cases into
>    multiple files inside "t5411/" like:
>
>        t5411/test-0000-normal-push.sh
>        t5411/test-0001-normal-push--porcelain.sh
>
> * Other minor fixes.

Thanks for a heads-up, but given that nobody seems to be reading
your patches and commenting on them, you might want to slow down a
bit.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 4/7] send-pack: extension for client-side status report
  2020-04-14 12:32                   ` [PATCH v12 4/7] send-pack: extension for client-side status report Jiang Xin
@ 2020-04-15 20:36                     ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-04-15 20:36 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> +		if (msg)
> +			hint->remote_status = xstrdup(msg);
> +		else if (extended_status)
> +			hint->remote_status = xstrdup(extended_status);

contrib/coccinelle/xstrdup_or_null.cocci suggests to rewrite the
above like so:

		if (msg)
			hint->remote_status = xstrdup(msg);
		else
			hint->remote_status = xstrdup_or_null(extended_status);

I'll queue a fix-up patch to keep 'pu' passing the test suite for now.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v3 0/5] fix git-push porcelain output and atomic report issue
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
  2020-03-29 14:35                   ` Jiang Xin
@ 2020-04-16 16:24                   ` Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 " Jiang Xin
                                       ` (5 more replies)
  2020-04-16 16:24                   ` [PATCH v3 1/5] send-pack: fix inconsistent porcelain output Jiang Xin
                                     ` (4 subsequent siblings)
  6 siblings, 6 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-16 16:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Find a new issue on porcelain output for git-push, add a new patch 1/5
to fix this issue.  This fix overlaps with and the topic `jx/atomic-push`,
so merge them together.

I find this issue when adding test cases of porcelain output for topic
`jx/proc-receive-hook`, which depends on this fix.

## Details of this new issue

The porcelain output of a failed `git-push` command is inconsistent for
different protocols.  For example, the following `git-push` command
failed because of the failure of `pre-receive` hook.

    git push --porcelain origin HEAD:refs/heads/master

For SSH protocol, the porcelain output does not end with a "Done"
message:

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)

While for HTTP protocol, the porcelain output does end with a "Done"
message;

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)
	Done

The following code at the end of function `send_pack()` indicates that
`send_pack()` should not return an error if some references are rejected
in porcelain mode.

    int send_pack(...)
        ... ...

        if (args->porcelain)
            return 0;

        for (ref = remote_refs; ref; ref = ref->next) {
            switch (ref->status) {
            case REF_STATUS_NONE:
            case REF_STATUS_UPTODATE:
            case REF_STATUS_OK:
                break;
            default:
                return -1;
            }
        }
        return 0;
    }

So if atomic push failed, must check the porcelain mode before return
an error.  And `receive_status()` should not return an error for a
failed updated reference, because `send_pack()` will check them instead.


## Range-diff v2...v3


-:  ---------- > 1:  d9ea3c35a3 send-pack: fix inconsistent porcelain output
1:  7a0579ba13 = 2:  bb07d5c330 t5543: never report what we do not push
2:  9b4bca8f4c ! 3:  1aa917b097 send-pack: mark failure of atomic push properly
    @@ t/t5543-atomic-push.sh: test_expect_failure 'atomic push reports (mirror, but re
      	(
      		cd workbench &&
     
    + ## t/t5548-push-porcelain.sh ##
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
    + 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
    + 	# git-push         : master(B)  bar(A)  NULL    next(A)
    +-	test_expect_success "atomic push failed ($PROTOCOL)" '
    ++	test_expect_failure "atomic push failed ($PROTOCOL)" '
    + 		(
    + 			cd workbench &&
    + 			git update-ref refs/heads/master $B &&
    +
      ## transport.c ##
     @@ transport.c: int transport_push(struct repository *r,
      		err = push_had_errors(remote_refs);
3:  a7e8d7c893 ! 4:  2848088852 transport-helper: mark failure for atomic push
    @@ t/t5541-http-push-smart.sh: test_expect_failure 'push --atomic also prevents bra
      
      test_expect_success 'push --atomic fails on server-side errors' '
     
    + ## t/t5548-push-porcelain.sh ##
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
    + 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
    + 	# git-push         : master(B)  bar(A)  NULL    next(A)
    +-	test_expect_failure "atomic push failed ($PROTOCOL)" '
    ++	test_expect_success "atomic push failed ($PROTOCOL)" '
    + 		(
    + 			cd workbench &&
    + 			git update-ref refs/heads/master $B &&
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 		make_user_friendly_and_stable_output <out >actual &&
    + 		cat >expect <<-EOF &&
    + 		To <URL/of/upstream.git>
    ++		=    refs/heads/next:refs/heads/next    [up to date]
    + 		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
    + 		!    (delete):refs/heads/baz    [rejected] (atomic push failed)
    + 		!    refs/heads/master:refs/heads/master    [rejected] (atomic push failed)
    +-		!    refs/heads/next:refs/heads/next    [rejected] (atomic push failed)
    + 		Done
    + 		EOF
    + 		test_cmp expect actual &&
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 		EOF
    + 		test_cmp expect actual
    + 	'
    +-
    + 	test_expect_success "prepare pre-receive hook ($PROTOCOL)" '
    + 		write_script "$upstream/hooks/pre-receive" <<-EOF
    + 		exit 1
    +
      ## transport-helper.c ##
     @@ transport-helper.c: static int push_refs_with_push(struct transport *transport,
      		case REF_STATUS_REJECT_STALE:
4:  94b13f5dcd ! 5:  d2f0b50395 transport-helper: new method reject_atomic_push()
    @@ send-pack.c: int send_pack(struct send_pack_args *args,
      			if (use_atomic) {
      				strbuf_release(&req_buf);
      				strbuf_release(&cap_buf);
    --				return atomic_push_failure(args, remote_refs, ref);
    +-				atomic_push_failure(args, remote_refs, ref);
     +				reject_atomic_push(remote_refs, args->send_mirror);
    -+				return error("atomic push failed for ref %s. status: %d\n",
    -+					     ref->name, ref->status);
    ++				error("atomic push failed for ref %s. status: %d\n",
    ++				      ref->name, ref->status);
    + 				return args->porcelain ? 0 : -1;
      			}
      			/* else fallthrough */
    - 		default:
     
      ## transport-helper.c ##
     @@ transport-helper.c: static int push_refs_with_push(struct transport *transport,

---

Jiang Xin (5):
  send-pack: fix inconsistent porcelain output
  t5543: never report what we do not push
  send-pack: mark failure of atomic push properly
  transport-helper: mark failure for atomic push
  transport-helper: new method reject_atomic_push()

 send-pack.c                |  32 +---
 t/t5541-http-push-smart.sh |  12 +-
 t/t5543-atomic-push.sh     |  89 +++++++++++
 t/t5548-push-porcelain.sh  | 300 +++++++++++++++++++++++++++++++++++++
 transport-helper.c         |  23 +++
 transport.c                |  24 ++-
 transport.h                |   3 +
 7 files changed, 439 insertions(+), 44 deletions(-)
 create mode 100755 t/t5548-push-porcelain.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v3 1/5] send-pack: fix inconsistent porcelain output
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
  2020-03-29 14:35                   ` Jiang Xin
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
@ 2020-04-16 16:24                   ` Jiang Xin
  2020-04-16 16:24                   ` [PATCH v3 2/5] t5543: never report what we do not push Jiang Xin
                                     ` (3 subsequent siblings)
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-16 16:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The porcelain output of a failed `git-push` command is inconsistent for
different protocols.  For example, the following `git-push` command
failed because of the failure of `pre-receive` hook.

    git push --porcelain origin HEAD:refs/heads/master

For SSH protocol, the porcelain output does not end with a "Done"
message:

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)

While for HTTP protocol, the porcelain output does end with a "Done"
message;

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)
	Done

The following code at the end of function `send_pack()` indicates that
`send_pack()` should not return an error if some references are rejected
in porcelain mode.

    int send_pack(...)
        ... ...

        if (args->porcelain)
            return 0;

        for (ref = remote_refs; ref; ref = ref->next) {
            switch (ref->status) {
            case REF_STATUS_NONE:
            case REF_STATUS_UPTODATE:
            case REF_STATUS_OK:
                break;
            default:
                return -1;
            }
        }
        return 0;
    }

So if atomic push failed, must check the porcelain mode before return
an error.  And `receive_status()` should not return an error for a
failed updated reference, because `send_pack()` will check them instead.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c               |   7 +-
 t/t5548-push-porcelain.sh | 301 ++++++++++++++++++++++++++++++++++++++
 transport.c               |  10 +-
 3 files changed, 313 insertions(+), 5 deletions(-)
 create mode 100755 t/t5548-push-porcelain.sh

diff --git a/send-pack.c b/send-pack.c
index 0407841ae8..1835cd5582 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -190,10 +190,8 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 
 		if (reader->line[0] == 'o' && reader->line[1] == 'k')
 			hint->status = REF_STATUS_OK;
-		else {
+		else
 			hint->status = REF_STATUS_REMOTE_REJECT;
-			ret = -1;
-		}
 		hint->remote_status = xstrdup_or_null(msg);
 		/* start our next search from the next ref */
 		hint = hint->next;
@@ -489,7 +487,8 @@ int send_pack(struct send_pack_args *args,
 			if (use_atomic) {
 				strbuf_release(&req_buf);
 				strbuf_release(&cap_buf);
-				return atomic_push_failure(args, remote_refs, ref);
+				atomic_push_failure(args, remote_refs, ref);
+				return args->porcelain ? 0 : -1;
 			}
 			/* else fallthrough */
 		default:
diff --git a/t/t5548-push-porcelain.sh b/t/t5548-push-porcelain.sh
new file mode 100755
index 0000000000..8d487b2a7a
--- /dev/null
+++ b/t/t5548-push-porcelain.sh
@@ -0,0 +1,301 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+test_description='Test git push porcelain output'
+
+. ./test-lib.sh
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#"
+}
+
+run_git_push_porcelain_output_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	file)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Refs of upstream : master(B)  foo(A)  bar(A)  baz(A)
+	# Refs of workbench: master(A)                  baz(A)  next(A)
+	# git-push         : master(A)  NULL    (B)     baz(A)  next(A)
+	test_expect_success "porcelain output of successful git-push ($PROTOCOL)" '
+		(
+			cd workbench &&
+			git update-ref refs/heads/master $A &&
+			git update-ref refs/heads/baz $A &&
+			git update-ref refs/heads/next $A &&
+			git push --porcelain --force origin \
+				master \
+				:refs/heads/foo \
+				$B:bar \
+				baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		=    refs/heads/baz:refs/heads/baz    [up to date]
+		     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+		-    :refs/heads/foo    [deleted]
+		+    refs/heads/master:refs/heads/master    <OID-B>...<OID-A> (forced update)
+		*    refs/heads/next:refs/heads/next    [new branch]
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-A> refs/heads/baz
+		<COMMIT-A> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+
+	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
+	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
+	# git-push         : master(B)  bar(A)  NULL    next(A)
+	test_expect_success "atomic push failed ($PROTOCOL)" '
+		(
+			cd workbench &&
+			git update-ref refs/heads/master $B &&
+			git update-ref refs/heads/bar $A &&
+			test_must_fail git push --atomic --porcelain origin \
+				master \
+				bar \
+				:baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
+		!    (delete):refs/heads/baz    [rejected] (atomic push failed)
+		!    refs/heads/master:refs/heads/master    [rejected] (atomic push failed)
+		!    refs/heads/next:refs/heads/next    [rejected] (atomic push failed)
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-A> refs/heads/baz
+		<COMMIT-A> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+
+	test_expect_success "prepare pre-receive hook ($PROTOCOL)" '
+		write_script "$upstream/hooks/pre-receive" <<-EOF
+		exit 1
+		EOF
+	'
+
+	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
+	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
+	# git-push         : master(B)  bar(A)  NULL    next(A)
+	test_expect_success "pre-receive hook declined ($PROTOCOL)" '
+		(
+			cd workbench &&
+			git update-ref refs/heads/master $B &&
+			git update-ref refs/heads/bar $A &&
+			test_must_fail git push --porcelain --force origin \
+				master \
+				bar \
+				:baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		=    refs/heads/next:refs/heads/next    [up to date]
+		!    refs/heads/bar:refs/heads/bar    [remote rejected] (pre-receive hook declined)
+		!    :refs/heads/baz    [remote rejected] (pre-receive hook declined)
+		!    refs/heads/master:refs/heads/master    [remote rejected] (pre-receive hook declined)
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-A> refs/heads/baz
+		<COMMIT-A> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+
+	test_expect_success "remove pre-receive hook ($PROTOCOL)" '
+		rm "$upstream/hooks/pre-receive"
+	'
+
+	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
+	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
+	# git-push         : master(B)  bar(A)  NULL    next(A)
+	test_expect_success "non-fastforward push ($PROTOCOL)" '
+		(
+			cd workbench &&
+			test_must_fail git push --porcelain origin \
+				master \
+				bar \
+				:baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		=    refs/heads/next:refs/heads/next    [up to date]
+		-    :refs/heads/baz    [deleted]
+		     refs/heads/master:refs/heads/master    <OID-A>..<OID-B>
+		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-B> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+
+	# upstream  (before) : master(A)  bar(B)  baz(A)  next(A)
+	# upstream  (after)  : master(B)  bar(A)  baz(A)           foo(A)
+	# workbench (before) : master(B)  bar(A)  baz(A)  next(A)
+	# workbench (after)  : master(A)
+	test_expect_success "cleanup ($PROTOCOL)" '
+		(
+			cd "$upstream" &&
+			git update-ref refs/heads/master $B &&
+			git update-ref refs/heads/foo    $A &&
+			git update-ref refs/heads/bar    $A &&
+			git update-ref refs/heads/baz    $A &&
+			git update-ref -d refs/heads/next
+		) &&
+		(
+			cd "workbench" &&
+			git update-ref refs/heads/master $A &&
+			git update-ref -d refs/heads/bar &&
+			git update-ref -d refs/heads/baz &&
+			git update-ref -d refs/heads/next
+		)
+	'
+}
+
+# Upstream  after setup : master(B)  foo(A)  bar(A)  baz(A)
+# Workbench after setup : master(A)
+test_expect_success "setup" '
+	git init --bare upstream.git &&
+	git init workbench &&
+	create_commits_in workbench A B &&
+	(
+		cd workbench &&
+		# Try to make a stable fixed width for abbreviated commit ID,
+		# this fixed-width oid will be replaced with "<OID>".
+		git config core.abbrev 7 &&
+		git remote add origin ../upstream.git &&
+		git update-ref refs/heads/master $A &&
+		git push origin \
+			$B:refs/heads/master \
+			$A:refs/heads/foo \
+			$A:refs/heads/bar \
+			$A:refs/heads/baz
+	) &&
+	git -C "workbench" config advice.pushUpdateRejected false &&
+	upstream=upstream.git
+'
+
+# Run git-push porcelain test on builtin protocol
+run_git_push_porcelain_output_test file
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+test_expect_success "setup for http" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Run git-push porcelain test on HTTP protocol
+run_git_push_porcelain_output_test http
+
+test_done
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..13d638d5fe 100644
--- a/transport.c
+++ b/transport.c
@@ -715,7 +715,15 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re
 
 	close(data->fd[1]);
 	close(data->fd[0]);
-	ret |= finish_connect(data->conn);
+	/*
+	 * Atomic push may abort the connection early and close the pipe,
+	 * which may cause an error for `finish_connect()`. Ignore this error
+	 * for atomic git-push.
+	 */
+	if (ret || args.atomic)
+		finish_connect(data->conn);
+	else
+		ret = finish_connect(data->conn);
 	data->conn = NULL;
 	data->got_remote_heads = 0;
 
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 2/5] t5543: never report what we do not push
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
                                     ` (2 preceding siblings ...)
  2020-04-16 16:24                   ` [PATCH v3 1/5] send-pack: fix inconsistent porcelain output Jiang Xin
@ 2020-04-16 16:24                   ` Jiang Xin
  2020-04-16 16:24                   ` [PATCH v3 3/5] send-pack: mark failure of atomic push properly Jiang Xin
                                     ` (2 subsequent siblings)
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-16 16:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When we push some references to the git server, we expect git to report
the status of the references we are pushing; no more, no less.  But when
pusing with atomic mode, if some references cannot be pushed, Git reports
the reject message on all references in the remote repository.

Add new test cases in t5543, and fix them in latter commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5543-atomic-push.sh | 89 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 89 insertions(+)

diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 7079bcf9a0..001240eec7 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -27,6 +27,12 @@ test_refs () {
 	test_cmp expect actual
 }
 
+fmt_status_report () {
+	sed -n \
+		-e "/^To / { s/   */ /g; p; }" \
+		-e "/^ ! / { s/   */ /g; p; }"
+}
+
 test_expect_success 'atomic push works for a single branch' '
 	mk_repo_pair &&
 	(
@@ -191,4 +197,87 @@ test_expect_success 'atomic push is not advertised if configured' '
 	test_refs master HEAD@{1}
 '
 
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+# Atomic push            : master(2)               two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by update hook)' '
+	mk_repo_pair &&
+	(
+		cd workbench &&
+		test_commit one &&
+		git branch foo &&
+		git push up master one foo &&
+		git tag -d one
+	) &&
+	(
+		mkdir -p upstream/.git/hooks &&
+		cat >upstream/.git/hooks/update <<-EOF &&
+		#!/bin/sh
+
+		if test "\$1" = "refs/heads/bar"
+		then
+			echo >&2 "Pusing to branch bar is prohibited"
+			exit 1
+		fi
+		EOF
+		chmod a+x upstream/.git/hooks/update
+	) &&
+	(
+		cd workbench &&
+		test_commit two &&
+		git branch bar
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master two bar >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] two -> two (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+	(
+		cd workbench &&
+		git remote remove up &&
+		git remote add up ../upstream
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic --mirror up >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] one (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	 ! [remote rejected] two -> two (atomic push failure)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(2) one(1) foo(1)
+# References in workbench: master(1)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by non-ff)' '
+	rm upstream/.git/hooks/update &&
+	(
+		cd workbench &&
+		git push up master &&
+		git reset --hard HEAD^
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master foo bar >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [rejected] master -> master (non-fast-forward)
+	 ! [rejected] bar -> bar (atomic push failed)
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 3/5] send-pack: mark failure of atomic push properly
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
                                     ` (3 preceding siblings ...)
  2020-04-16 16:24                   ` [PATCH v3 2/5] t5543: never report what we do not push Jiang Xin
@ 2020-04-16 16:24                   ` Jiang Xin
  2020-04-16 16:24                   ` [PATCH v3 4/5] transport-helper: mark failure for atomic push Jiang Xin
  2020-04-16 16:24                   ` [PATCH v3 5/5] transport-helper: new method reject_atomic_push() Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-16 16:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing with SSH or other smart protocol, references are validated
by function `check_to_send_update()` before they are sent in commands
to `send_pack()` of "receve-pack".  For atomic push, if a reference is
rejected after the validation, only references pushed by user should be
marked as failure, instead of report failure on all remote references.

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) wanted to fix report issue of HTTP
protocol, but marked all remote references failure for atomic push.

In order to fix the issue of status report for SSH or other built-in
smart protocol, revert part of that commit and add additional status
for function `atomic_push_failure()`.  The additional status for it
except the "REF_STATUS_EXPECTING_REPORT" status are:

- REF_STATUS_NONE : Not marked as "REF_STATUS_EXPECTING_REPORT" yet.
- REF_STATUS_OK   : Assume OK for dryrun or status_report is disabled.

This fix won't resolve the issue of status report in transport-helper
for HTTP or other protocols, and breaks test case in t5541.  Will fix
it in additional commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c                |  2 ++
 t/t5541-http-push-smart.sh |  2 +-
 t/t5543-atomic-push.sh     |  6 +++---
 t/t5548-push-porcelain.sh  |  2 +-
 transport.c                | 14 --------------
 5 files changed, 7 insertions(+), 19 deletions(-)

diff --git a/send-pack.c b/send-pack.c
index 1835cd5582..efefb687b2 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -332,6 +332,8 @@ static int atomic_push_failure(struct send_pack_args *args,
 			continue;
 
 		switch (ref->status) {
+		case REF_STATUS_NONE:
+		case REF_STATUS_OK:
 		case REF_STATUS_EXPECTING_REPORT:
 			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
 			continue;
diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 23be8ce92d..2c2c3fb0f5 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,7 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
+test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 001240eec7..620c30d58f 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -200,7 +200,7 @@ test_expect_success 'atomic push is not advertised if configured' '
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
 # Atomic push            : master(2)               two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by update hook)' '
+test_expect_success 'atomic push reports (reject by update hook)' '
 	mk_repo_pair &&
 	(
 		cd workbench &&
@@ -241,7 +241,7 @@ test_expect_failure 'atomic push reports (reject by update hook)' '
 
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+test_expect_success 'atomic push reports (mirror, but reject by update hook)' '
 	(
 		cd workbench &&
 		git remote remove up &&
@@ -262,7 +262,7 @@ test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
 
 # References in upstream : master(2) one(1) foo(1)
 # References in workbench: master(1)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by non-ff)' '
+test_expect_success 'atomic push reports (reject by non-ff)' '
 	rm upstream/.git/hooks/update &&
 	(
 		cd workbench &&
diff --git a/t/t5548-push-porcelain.sh b/t/t5548-push-porcelain.sh
index 8d487b2a7a..e7d9e06dac 100755
--- a/t/t5548-push-porcelain.sh
+++ b/t/t5548-push-porcelain.sh
@@ -110,7 +110,7 @@ run_git_push_porcelain_output_test() {
 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
 	# git-push         : master(B)  bar(A)  NULL    next(A)
-	test_expect_success "atomic push failed ($PROTOCOL)" '
+	test_expect_failure "atomic push failed ($PROTOCOL)" '
 		(
 			cd workbench &&
 			git update-ref refs/heads/master $B &&
diff --git a/transport.c b/transport.c
index 13d638d5fe..a07e39564c 100644
--- a/transport.c
+++ b/transport.c
@@ -1248,20 +1248,6 @@ int transport_push(struct repository *r,
 		err = push_had_errors(remote_refs);
 		ret = push_ret | err;
 
-		if ((flags & TRANSPORT_PUSH_ATOMIC) && err) {
-			struct ref *it;
-			for (it = remote_refs; it; it = it->next)
-				switch (it->status) {
-				case REF_STATUS_NONE:
-				case REF_STATUS_UPTODATE:
-				case REF_STATUS_OK:
-					it->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-					break;
-				default:
-					break;
-				}
-		}
-
 		if (!quiet || err)
 			transport_print_push_status(transport->url, remote_refs,
 					verbose | porcelain, porcelain,
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 4/5] transport-helper: mark failure for atomic push
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
                                     ` (4 preceding siblings ...)
  2020-04-16 16:24                   ` [PATCH v3 3/5] send-pack: mark failure of atomic push properly Jiang Xin
@ 2020-04-16 16:24                   ` Jiang Xin
  2020-04-16 16:24                   ` [PATCH v3 5/5] transport-helper: new method reject_atomic_push() Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-16 16:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) noticed the incomplete report of
failure of an atomic push for HTTP protocol.  But the implementation
has a flaw that mark all remote references as failure.

Only mark necessary references as failure in `push_refs_with_push()` of
transport-helper.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5541-http-push-smart.sh | 14 +++++++++++---
 t/t5548-push-porcelain.sh  |  5 ++---
 transport-helper.c         | 15 +++++++++++++++
 3 files changed, 28 insertions(+), 6 deletions(-)

diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 2c2c3fb0f5..afc680d5e3 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,10 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
+## References of remote: atomic1(1)            master(2) collateral(2) other(2)
+## References of local :            atomic2(2) master(1) collateral(3) other(2) collateral1(3) atomic(1)
+## Atomic push         :                       master(1) collateral(3)                         atomic(1)
+test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
@@ -189,7 +192,8 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 	test_commit atomic2 &&
 	git branch collateral &&
 	git branch other &&
-	git push "$up" master collateral other &&
+	git push "$up" atomic1 master collateral other &&
+	git tag -d atomic1 &&
 
 	# collateral is a valid push, but should be failed by atomic push
 	git checkout collateral &&
@@ -224,7 +228,11 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 
 	# the collateral failure refs should be indicated to the user
 	grep "^ ! .*rejected.* atomic -> atomic .*atomic push failed" output &&
-	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output
+	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output &&
+
+	# never report what we do not push
+	! grep "^ ! .*rejected.* atomic1 " output &&
+	! grep "^ ! .*rejected.* other " output
 '
 
 test_expect_success 'push --atomic fails on server-side errors' '
diff --git a/t/t5548-push-porcelain.sh b/t/t5548-push-porcelain.sh
index e7d9e06dac..898099aede 100755
--- a/t/t5548-push-porcelain.sh
+++ b/t/t5548-push-porcelain.sh
@@ -110,7 +110,7 @@ run_git_push_porcelain_output_test() {
 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
 	# git-push         : master(B)  bar(A)  NULL    next(A)
-	test_expect_failure "atomic push failed ($PROTOCOL)" '
+	test_expect_success "atomic push failed ($PROTOCOL)" '
 		(
 			cd workbench &&
 			git update-ref refs/heads/master $B &&
@@ -124,10 +124,10 @@ run_git_push_porcelain_output_test() {
 		make_user_friendly_and_stable_output <out >actual &&
 		cat >expect <<-EOF &&
 		To <URL/of/upstream.git>
+		=    refs/heads/next:refs/heads/next    [up to date]
 		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
 		!    (delete):refs/heads/baz    [rejected] (atomic push failed)
 		!    refs/heads/master:refs/heads/master    [rejected] (atomic push failed)
-		!    refs/heads/next:refs/heads/next    [rejected] (atomic push failed)
 		Done
 		EOF
 		test_cmp expect actual &&
@@ -142,7 +142,6 @@ run_git_push_porcelain_output_test() {
 		EOF
 		test_cmp expect actual
 	'
-
 	test_expect_success "prepare pre-receive hook ($PROTOCOL)" '
 		write_script "$upstream/hooks/pre-receive" <<-EOF
 		exit 1
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..ab3b52eb14 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -894,6 +894,21 @@ static int push_refs_with_push(struct transport *transport,
 		case REF_STATUS_REJECT_STALE:
 		case REF_STATUS_REJECT_ALREADY_EXISTS:
 			if (atomic) {
+				/* Mark other refs as failed */
+				for (ref = remote_refs; ref; ref = ref->next) {
+					if (!ref->peer_ref && !mirror)
+						continue;
+
+					switch (ref->status) {
+					case REF_STATUS_NONE:
+					case REF_STATUS_OK:
+					case REF_STATUS_EXPECTING_REPORT:
+						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+						continue;
+					default:
+						break; /* do nothing */
+					}
+				}
 				string_list_clear(&cas_options, 0);
 				return 0;
 			} else
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 5/5] transport-helper: new method reject_atomic_push()
  2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
                                     ` (5 preceding siblings ...)
  2020-04-16 16:24                   ` [PATCH v3 4/5] transport-helper: mark failure for atomic push Jiang Xin
@ 2020-04-16 16:24                   ` Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-16 16:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new method in transport-helper to reject all references if any
reference is failed for atomic push.

This method is reused in "send-pack.c" and "transport-helper.c", one for
SSH, git and file protocols, and the other for HTTP protocol.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c        | 29 +++--------------------------
 transport-helper.c | 38 +++++++++++++++++++++++---------------
 transport.h        |  3 +++
 3 files changed, 29 insertions(+), 41 deletions(-)

diff --git a/send-pack.c b/send-pack.c
index efefb687b2..a7c53193c9 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -320,31 +320,6 @@ static int generate_push_cert(struct strbuf *req_buf,
 	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_NONE:
-		case REF_STATUS_OK:
-		case REF_STATUS_EXPECTING_REPORT:
-			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-			continue;
-		default:
-			break; /* do nothing */
-		}
-	}
-	return error("atomic push failed for ref %s. status: %d\n",
-		     failing_ref->name, failing_ref->status);
-}
-
 #define NONCE_LEN_LIMIT 256
 
 static void reject_invalid_nonce(const char *nonce, int len)
@@ -489,7 +464,9 @@ int send_pack(struct send_pack_args *args,
 			if (use_atomic) {
 				strbuf_release(&req_buf);
 				strbuf_release(&cap_buf);
-				atomic_push_failure(args, remote_refs, ref);
+				reject_atomic_push(remote_refs, args->send_mirror);
+				error("atomic push failed for ref %s. status: %d\n",
+				      ref->name, ref->status);
 				return args->porcelain ? 0 : -1;
 			}
 			/* else fallthrough */
diff --git a/transport-helper.c b/transport-helper.c
index ab3b52eb14..a46afcb69d 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -894,21 +894,7 @@ static int push_refs_with_push(struct transport *transport,
 		case REF_STATUS_REJECT_STALE:
 		case REF_STATUS_REJECT_ALREADY_EXISTS:
 			if (atomic) {
-				/* Mark other refs as failed */
-				for (ref = remote_refs; ref; ref = ref->next) {
-					if (!ref->peer_ref && !mirror)
-						continue;
-
-					switch (ref->status) {
-					case REF_STATUS_NONE:
-					case REF_STATUS_OK:
-					case REF_STATUS_EXPECTING_REPORT:
-						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-						continue;
-					default:
-						break; /* do nothing */
-					}
-				}
+				reject_atomic_push(remote_refs, mirror);
 				string_list_clear(&cas_options, 0);
 				return 0;
 			} else
@@ -1503,3 +1489,25 @@ int bidirectional_transfer_loop(int input, int output)
 
 	return tloop_spawnwait_tasks(&state);
 }
+
+void reject_atomic_push(struct ref *remote_refs, int mirror_mode)
+{
+	struct ref *ref;
+
+	/* Mark other refs as failed */
+	for (ref = remote_refs; ref; ref = ref->next) {
+		if (!ref->peer_ref && !mirror_mode)
+			continue;
+
+		switch (ref->status) {
+		case REF_STATUS_NONE:
+		case REF_STATUS_OK:
+		case REF_STATUS_EXPECTING_REPORT:
+			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+			continue;
+		default:
+			break; /* do nothing */
+		}
+	}
+	return;
+}
diff --git a/transport.h b/transport.h
index e0131daab9..4298c855be 100644
--- a/transport.h
+++ b/transport.h
@@ -265,4 +265,7 @@ int transport_refs_pushed(struct ref *ref);
 void transport_print_push_status(const char *dest, struct ref *refs,
 		  int verbose, int porcelain, unsigned int *reject_reasons);
 
+/* common method used by transport-helper.c and send-pack.c */
+void reject_atomic_push(struct ref *refs, int mirror_mode);
+
 #endif
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 0/5] fix git-push porcelain output and atomic report issue
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
@ 2020-04-17  9:45                     ` Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 1/5] send-pack: fix inconsistent porcelain output Jiang Xin
                                       ` (4 subsequent siblings)
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-17  9:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Patch 1/5 fixes format of porcelain output for `git-push`, and it should
belong to a new topic, but it has a overlap with topic `jx/atomic-push`.

So I rebased `jx/atomic-push` on top of it, and resolved conflicts
between these two features.  Patch 2/5 ~ 5/5 are belong to the original
topic `jx/atomic-push`.

Note: Some patches of `jx/proc-receive-hook` also have overlap with this
      patch series, so v13 of `jx/proc-receive-hook` will base on this
      topic, instead of master branch of Git.

## Changes since `jx/atomic-push` v3:

* Fix broken test cases in t5504, and t5516, which is instroduced by
  patch 1/5..

## Changes since `jx/atomic-push` v2:

Add new patch 1/5 which fixes new issue found in git-push porcelain output.
This fix overlaps with and the topic `jx/atomic-push`, so merge them together.

I find this issue when adding test cases of porcelain output for topic
`jx/proc-receive-hook`, which depends on this fix.

Details of this new issue:

The porcelain output of a failed `git-push` command is inconsistent for
different protocols.  For example, the following `git-push` command
may fail due to the failure of the `pre-receive` hook.

    git push --porcelain origin HEAD:refs/heads/master

For SSH protocol, the porcelain output does not end with a "Done"
message:

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)

While for HTTP protocol, the porcelain output does end with a "Done"
message:

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)
	Done

The following code at the end of function `send_pack()` indicates that
`send_pack()` should not return an error if some references are rejected
in porcelain mode.

    int send_pack(...)
        ... ...

        if (args->porcelain)
            return 0;

        for (ref = remote_refs; ref; ref = ref->next) {
            switch (ref->status) {
            case REF_STATUS_NONE:
            case REF_STATUS_UPTODATE:
            case REF_STATUS_OK:
                break;
            default:
                return -1;
            }
        }
        return 0;
    }

So if atomic push failed, must check the porcelain mode before return
an error.  And `receive_status()` should not return an error for a
failed updated reference, because `send_pack()` will check them instead.


## Range-diff v2...v4

-:  ---------- > 1:  96028ebf7d send-pack: fix inconsistent porcelain output
1:  7a0579ba13 = 2:  77f66ebfd2 t5543: never report what we do not push
2:  9b4bca8f4c ! 3:  cb63c4a9f4 send-pack: mark failure of atomic push properly
    @@ t/t5543-atomic-push.sh: test_expect_failure 'atomic push reports (mirror, but re
      	(
      		cd workbench &&
     
    + ## t/t5548-push-porcelain.sh ##
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
    + 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
    + 	# git-push         : master(B)  bar(A)  NULL    next(A)
    +-	test_expect_success "atomic push failed ($PROTOCOL)" '
    ++	test_expect_failure "atomic push failed ($PROTOCOL)" '
    + 		(
    + 			cd workbench &&
    + 			git update-ref refs/heads/master $B &&
    +
      ## transport.c ##
     @@ transport.c: int transport_push(struct repository *r,
      		err = push_had_errors(remote_refs);
3:  a7e8d7c893 ! 4:  3fb57569da transport-helper: mark failure for atomic push
    @@ t/t5541-http-push-smart.sh: test_expect_failure 'push --atomic also prevents bra
      
      test_expect_success 'push --atomic fails on server-side errors' '
     
    + ## t/t5548-push-porcelain.sh ##
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
    + 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
    + 	# git-push         : master(B)  bar(A)  NULL    next(A)
    +-	test_expect_failure "atomic push failed ($PROTOCOL)" '
    ++	test_expect_success "atomic push failed ($PROTOCOL)" '
    + 		(
    + 			cd workbench &&
    + 			git update-ref refs/heads/master $B &&
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 		make_user_friendly_and_stable_output <out >actual &&
    + 		cat >expect <<-EOF &&
    + 		To <URL/of/upstream.git>
    ++		=    refs/heads/next:refs/heads/next    [up to date]
    + 		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
    + 		!    (delete):refs/heads/baz    [rejected] (atomic push failed)
    + 		!    refs/heads/master:refs/heads/master    [rejected] (atomic push failed)
    +-		!    refs/heads/next:refs/heads/next    [rejected] (atomic push failed)
    + 		Done
    + 		EOF
    + 		test_cmp expect actual &&
    +@@ t/t5548-push-porcelain.sh: run_git_push_porcelain_output_test() {
    + 		EOF
    + 		test_cmp expect actual
    + 	'
    +-
    + 	test_expect_success "prepare pre-receive hook ($PROTOCOL)" '
    + 		write_script "$upstream/hooks/pre-receive" <<-EOF
    + 		exit 1
    +
      ## transport-helper.c ##
     @@ transport-helper.c: static int push_refs_with_push(struct transport *transport,
      		case REF_STATUS_REJECT_STALE:
4:  94b13f5dcd ! 5:  1c73ddff0d transport-helper: new method reject_atomic_push()
    @@ send-pack.c: int send_pack(struct send_pack_args *args,
      			if (use_atomic) {
      				strbuf_release(&req_buf);
      				strbuf_release(&cap_buf);
    --				return atomic_push_failure(args, remote_refs, ref);
    +-				atomic_push_failure(args, remote_refs, ref);
     +				reject_atomic_push(remote_refs, args->send_mirror);
    -+				return error("atomic push failed for ref %s. status: %d\n",
    -+					     ref->name, ref->status);
    ++				error("atomic push failed for ref %s. status: %d\n",
    ++				      ref->name, ref->status);
    + 				return args->porcelain ? 0 : -1;
      			}
      			/* else fallthrough */
    - 		default:
     
      ## transport-helper.c ##
     @@ transport-helper.c: static int push_refs_with_push(struct transport *transport,

---

Jiang Xin (5):
  send-pack: fix inconsistent porcelain output
  t5543: never report what we do not push
  send-pack: mark failure of atomic push properly
  transport-helper: mark failure for atomic push
  transport-helper: new method reject_atomic_push()

 send-pack.c                     |  32 +---
 t/t5504-fetch-receive-strict.sh |   1 +
 t/t5516-fetch-push.sh           |   1 +
 t/t5541-http-push-smart.sh      |  12 +-
 t/t5543-atomic-push.sh          |  89 ++++++++++
 t/t5548-push-porcelain.sh       | 300 ++++++++++++++++++++++++++++++++
 transport-helper.c              |  23 +++
 transport.c                     |  24 +--
 transport.h                     |   3 +
 9 files changed, 441 insertions(+), 44 deletions(-)
 create mode 100755 t/t5548-push-porcelain.sh

-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v4 1/5] send-pack: fix inconsistent porcelain output
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 " Jiang Xin
@ 2020-04-17  9:45                     ` Jiang Xin
  2020-04-17 19:51                       ` Junio C Hamano
  2020-04-17  9:45                     ` [PATCH v4 2/5] t5543: never report what we do not push Jiang Xin
                                       ` (3 subsequent siblings)
  5 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-17  9:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The porcelain output of a failed `git-push` command is inconsistent for
different protocols.  For example, the following `git-push` command
may fail due to the failure of the `pre-receive` hook.

    git push --porcelain origin HEAD:refs/heads/master

For SSH protocol, the porcelain output does not end with a "Done"
message:

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)

While for HTTP protocol, the porcelain output does end with a "Done"
message:

	To <URL/of/upstream.git>
	!  HEAD:refs/heads/master  [remote rejected] (pre-receive hook declined)
	Done

The following code at the end of function `send_pack()` indicates that
`send_pack()` should not return an error if some references are rejected
in porcelain mode.

    int send_pack(...)
        ... ...

        if (args->porcelain)
            return 0;

        for (ref = remote_refs; ref; ref = ref->next) {
            switch (ref->status) {
            case REF_STATUS_NONE:
            case REF_STATUS_UPTODATE:
            case REF_STATUS_OK:
                break;
            default:
                return -1;
            }
        }
        return 0;
    }

So if atomic push failed, must check the porcelain mode before return
an error.  And `receive_status()` should not return an error for a
failed updated reference, because `send_pack()` will check them instead.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c                     |   7 +-
 t/t5504-fetch-receive-strict.sh |   1 +
 t/t5516-fetch-push.sh           |   1 +
 t/t5548-push-porcelain.sh       | 280 ++++++++++++++++++++++++++++++++
 transport.c                     |  10 +-
 5 files changed, 294 insertions(+), 5 deletions(-)
 create mode 100755 t/t5548-push-porcelain.sh

diff --git a/send-pack.c b/send-pack.c
index 0407841ae8..1835cd5582 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -190,10 +190,8 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 
 		if (reader->line[0] == 'o' && reader->line[1] == 'k')
 			hint->status = REF_STATUS_OK;
-		else {
+		else
 			hint->status = REF_STATUS_REMOTE_REJECT;
-			ret = -1;
-		}
 		hint->remote_status = xstrdup_or_null(msg);
 		/* start our next search from the next ref */
 		hint = hint->next;
@@ -489,7 +487,8 @@ int send_pack(struct send_pack_args *args,
 			if (use_atomic) {
 				strbuf_release(&req_buf);
 				strbuf_release(&cap_buf);
-				return atomic_push_failure(args, remote_refs, ref);
+				atomic_push_failure(args, remote_refs, ref);
+				return args->porcelain ? 0 : -1;
 			}
 			/* else fallthrough */
 		default:
diff --git a/t/t5504-fetch-receive-strict.sh b/t/t5504-fetch-receive-strict.sh
index 645b4c78d3..a32efe2b6c 100755
--- a/t/t5504-fetch-receive-strict.sh
+++ b/t/t5504-fetch-receive-strict.sh
@@ -65,6 +65,7 @@ test_expect_success 'fetch with transfer.fsckobjects' '
 cat >exp <<EOF
 To dst
 !	refs/heads/master:refs/heads/test	[remote rejected] (missing necessary objects)
+Done
 EOF
 
 test_expect_success 'push without strict' '
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9ff041a093..9c6218f568 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1066,6 +1066,7 @@ test_expect_success 'push --porcelain rejected' '
 
 	echo >.git/foo  "To testrepo"  &&
 	echo >>.git/foo "!	refs/heads/master:refs/heads/master	[remote rejected] (branch is currently checked out)" &&
+	echo >>.git/foo "Done" &&
 
 	test_must_fail git push >.git/bar --porcelain  testrepo refs/heads/master:refs/heads/master &&
 	test_cmp .git/foo .git/bar
diff --git a/t/t5548-push-porcelain.sh b/t/t5548-push-porcelain.sh
new file mode 100755
index 0000000000..96ba449a38
--- /dev/null
+++ b/t/t5548-push-porcelain.sh
@@ -0,0 +1,280 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+test_description='Test git push porcelain output'
+
+. ./test-lib.sh
+
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#"
+}
+
+setup_upstream_and_workbench () {
+	# Upstream  after setup : master(B)  foo(A)  bar(A)  baz(A)
+	# Workbench after setup : master(A)
+	test_expect_success "setup upstream repository and workbench" '
+		rm -rf upstream.git workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git remote add origin ../upstream.git &&
+			git update-ref refs/heads/master $A &&
+			git push origin \
+				$B:refs/heads/master \
+				$A:refs/heads/foo \
+				$A:refs/heads/bar \
+				$A:refs/heads/baz
+		) &&
+		git -C "workbench" config advice.pushUpdateRejected false &&
+		upstream=upstream.git
+	'
+}
+
+run_git_push_porcelain_output_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	file)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Refs of upstream : master(B)  foo(A)  bar(A)  baz(A)
+	# Refs of workbench: master(A)                  baz(A)  next(A)
+	# git-push         : master(A)  NULL    (B)     baz(A)  next(A)
+	test_expect_success "porcelain output of successful git-push ($PROTOCOL)" '
+		(
+			cd workbench &&
+			git update-ref refs/heads/master $A &&
+			git update-ref refs/heads/baz $A &&
+			git update-ref refs/heads/next $A &&
+			git push --porcelain --force origin \
+				master \
+				:refs/heads/foo \
+				$B:bar \
+				baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		=    refs/heads/baz:refs/heads/baz    [up to date]
+		     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+		-    :refs/heads/foo    [deleted]
+		+    refs/heads/master:refs/heads/master    <OID-B>...<OID-A> (forced update)
+		*    refs/heads/next:refs/heads/next    [new branch]
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-A> refs/heads/baz
+		<COMMIT-A> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+
+	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
+	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
+	# git-push         : master(B)  bar(A)  NULL    next(A)
+	test_expect_success "atomic push failed ($PROTOCOL)" '
+		(
+			cd workbench &&
+			git update-ref refs/heads/master $B &&
+			git update-ref refs/heads/bar $A &&
+			test_must_fail git push --atomic --porcelain origin \
+				master \
+				bar \
+				:baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
+		!    (delete):refs/heads/baz    [rejected] (atomic push failed)
+		!    refs/heads/master:refs/heads/master    [rejected] (atomic push failed)
+		!    refs/heads/next:refs/heads/next    [rejected] (atomic push failed)
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-A> refs/heads/baz
+		<COMMIT-A> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+
+	test_expect_success "prepare pre-receive hook ($PROTOCOL)" '
+		write_script "$upstream/hooks/pre-receive" <<-EOF
+		exit 1
+		EOF
+	'
+
+	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
+	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
+	# git-push         : master(B)  bar(A)  NULL    next(A)
+	test_expect_success "pre-receive hook declined ($PROTOCOL)" '
+		(
+			cd workbench &&
+			git update-ref refs/heads/master $B &&
+			git update-ref refs/heads/bar $A &&
+			test_must_fail git push --porcelain --force origin \
+				master \
+				bar \
+				:baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		=    refs/heads/next:refs/heads/next    [up to date]
+		!    refs/heads/bar:refs/heads/bar    [remote rejected] (pre-receive hook declined)
+		!    :refs/heads/baz    [remote rejected] (pre-receive hook declined)
+		!    refs/heads/master:refs/heads/master    [remote rejected] (pre-receive hook declined)
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-A> refs/heads/baz
+		<COMMIT-A> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+
+	test_expect_success "remove pre-receive hook ($PROTOCOL)" '
+		rm "$upstream/hooks/pre-receive"
+	'
+
+	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
+	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
+	# git-push         : master(B)  bar(A)  NULL    next(A)
+	test_expect_success "non-fastforward push ($PROTOCOL)" '
+		(
+			cd workbench &&
+			test_must_fail git push --porcelain origin \
+				master \
+				bar \
+				:baz \
+				next
+		) >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		To <URL/of/upstream.git>
+		=    refs/heads/next:refs/heads/next    [up to date]
+		-    :refs/heads/baz    [deleted]
+		     refs/heads/master:refs/heads/master    <OID-A>..<OID-B>
+		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
+		Done
+		EOF
+		test_cmp expect actual &&
+
+		git -C "$upstream" show-ref >out &&
+		make_user_friendly_and_stable_output <out >actual &&
+		cat >expect <<-EOF &&
+		<COMMIT-B> refs/heads/bar
+		<COMMIT-B> refs/heads/master
+		<COMMIT-A> refs/heads/next
+		EOF
+		test_cmp expect actual
+	'
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run git-push porcelain test on builtin protocol
+run_git_push_porcelain_output_test file
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+test_expect_success "setup for http" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Run git-push porcelain test on HTTP protocol
+run_git_push_porcelain_output_test http
+
+test_done
diff --git a/transport.c b/transport.c
index 1fdc7dac1a..13d638d5fe 100644
--- a/transport.c
+++ b/transport.c
@@ -715,7 +715,15 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re
 
 	close(data->fd[1]);
 	close(data->fd[0]);
-	ret |= finish_connect(data->conn);
+	/*
+	 * Atomic push may abort the connection early and close the pipe,
+	 * which may cause an error for `finish_connect()`. Ignore this error
+	 * for atomic git-push.
+	 */
+	if (ret || args.atomic)
+		finish_connect(data->conn);
+	else
+		ret = finish_connect(data->conn);
 	data->conn = NULL;
 	data->got_remote_heads = 0;
 
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 2/5] t5543: never report what we do not push
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 " Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 1/5] send-pack: fix inconsistent porcelain output Jiang Xin
@ 2020-04-17  9:45                     ` Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 3/5] send-pack: mark failure of atomic push properly Jiang Xin
                                       ` (2 subsequent siblings)
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-17  9:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When we push some references to the git server, we expect git to report
the status of the references we are pushing; no more, no less.  But when
pusing with atomic mode, if some references cannot be pushed, Git reports
the reject message on all references in the remote repository.

Add new test cases in t5543, and fix them in latter commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5543-atomic-push.sh | 89 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 89 insertions(+)

diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 7079bcf9a0..001240eec7 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -27,6 +27,12 @@ test_refs () {
 	test_cmp expect actual
 }
 
+fmt_status_report () {
+	sed -n \
+		-e "/^To / { s/   */ /g; p; }" \
+		-e "/^ ! / { s/   */ /g; p; }"
+}
+
 test_expect_success 'atomic push works for a single branch' '
 	mk_repo_pair &&
 	(
@@ -191,4 +197,87 @@ test_expect_success 'atomic push is not advertised if configured' '
 	test_refs master HEAD@{1}
 '
 
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+# Atomic push            : master(2)               two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by update hook)' '
+	mk_repo_pair &&
+	(
+		cd workbench &&
+		test_commit one &&
+		git branch foo &&
+		git push up master one foo &&
+		git tag -d one
+	) &&
+	(
+		mkdir -p upstream/.git/hooks &&
+		cat >upstream/.git/hooks/update <<-EOF &&
+		#!/bin/sh
+
+		if test "\$1" = "refs/heads/bar"
+		then
+			echo >&2 "Pusing to branch bar is prohibited"
+			exit 1
+		fi
+		EOF
+		chmod a+x upstream/.git/hooks/update
+	) &&
+	(
+		cd workbench &&
+		test_commit two &&
+		git branch bar
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master two bar >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] two -> two (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(1) one(1) foo(1)
+# References in workbench: master(2)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+	(
+		cd workbench &&
+		git remote remove up &&
+		git remote add up ../upstream
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic --mirror up >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [remote rejected] master -> master (atomic push failure)
+	 ! [remote rejected] one (atomic push failure)
+	 ! [remote rejected] bar -> bar (hook declined)
+	 ! [remote rejected] two -> two (atomic push failure)
+	EOF
+	test_cmp expect actual
+'
+
+# References in upstream : master(2) one(1) foo(1)
+# References in workbench: master(1)        foo(1) two(2) bar(2)
+test_expect_failure 'atomic push reports (reject by non-ff)' '
+	rm upstream/.git/hooks/update &&
+	(
+		cd workbench &&
+		git push up master &&
+		git reset --hard HEAD^
+	) &&
+	test_must_fail git -C workbench \
+		push --atomic up master foo bar >out 2>&1 &&
+	fmt_status_report <out >actual &&
+	cat >expect <<-EOF &&
+	To ../upstream
+	 ! [rejected] master -> master (non-fast-forward)
+	 ! [rejected] bar -> bar (atomic push failed)
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 3/5] send-pack: mark failure of atomic push properly
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
                                       ` (2 preceding siblings ...)
  2020-04-17  9:45                     ` [PATCH v4 2/5] t5543: never report what we do not push Jiang Xin
@ 2020-04-17  9:45                     ` Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 4/5] transport-helper: mark failure for atomic push Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 5/5] transport-helper: new method reject_atomic_push() Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-17  9:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing with SSH or other smart protocol, references are validated
by function `check_to_send_update()` before they are sent in commands
to `send_pack()` of "receve-pack".  For atomic push, if a reference is
rejected after the validation, only references pushed by user should be
marked as failure, instead of report failure on all remote references.

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) wanted to fix report issue of HTTP
protocol, but marked all remote references failure for atomic push.

In order to fix the issue of status report for SSH or other built-in
smart protocol, revert part of that commit and add additional status
for function `atomic_push_failure()`.  The additional status for it
except the "REF_STATUS_EXPECTING_REPORT" status are:

- REF_STATUS_NONE : Not marked as "REF_STATUS_EXPECTING_REPORT" yet.
- REF_STATUS_OK   : Assume OK for dryrun or status_report is disabled.

This fix won't resolve the issue of status report in transport-helper
for HTTP or other protocols, and breaks test case in t5541.  Will fix
it in additional commit.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c                |  2 ++
 t/t5541-http-push-smart.sh |  2 +-
 t/t5543-atomic-push.sh     |  6 +++---
 t/t5548-push-porcelain.sh  |  2 +-
 transport.c                | 14 --------------
 5 files changed, 7 insertions(+), 19 deletions(-)

diff --git a/send-pack.c b/send-pack.c
index 1835cd5582..efefb687b2 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -332,6 +332,8 @@ static int atomic_push_failure(struct send_pack_args *args,
 			continue;
 
 		switch (ref->status) {
+		case REF_STATUS_NONE:
+		case REF_STATUS_OK:
 		case REF_STATUS_EXPECTING_REPORT:
 			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
 			continue;
diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 23be8ce92d..2c2c3fb0f5 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,7 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
+test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
diff --git a/t/t5543-atomic-push.sh b/t/t5543-atomic-push.sh
index 001240eec7..620c30d58f 100755
--- a/t/t5543-atomic-push.sh
+++ b/t/t5543-atomic-push.sh
@@ -200,7 +200,7 @@ test_expect_success 'atomic push is not advertised if configured' '
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
 # Atomic push            : master(2)               two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by update hook)' '
+test_expect_success 'atomic push reports (reject by update hook)' '
 	mk_repo_pair &&
 	(
 		cd workbench &&
@@ -241,7 +241,7 @@ test_expect_failure 'atomic push reports (reject by update hook)' '
 
 # References in upstream : master(1) one(1) foo(1)
 # References in workbench: master(2)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
+test_expect_success 'atomic push reports (mirror, but reject by update hook)' '
 	(
 		cd workbench &&
 		git remote remove up &&
@@ -262,7 +262,7 @@ test_expect_failure 'atomic push reports (mirror, but reject by update hook)' '
 
 # References in upstream : master(2) one(1) foo(1)
 # References in workbench: master(1)        foo(1) two(2) bar(2)
-test_expect_failure 'atomic push reports (reject by non-ff)' '
+test_expect_success 'atomic push reports (reject by non-ff)' '
 	rm upstream/.git/hooks/update &&
 	(
 		cd workbench &&
diff --git a/t/t5548-push-porcelain.sh b/t/t5548-push-porcelain.sh
index 96ba449a38..9f4b7de74b 100755
--- a/t/t5548-push-porcelain.sh
+++ b/t/t5548-push-porcelain.sh
@@ -136,7 +136,7 @@ run_git_push_porcelain_output_test() {
 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
 	# git-push         : master(B)  bar(A)  NULL    next(A)
-	test_expect_success "atomic push failed ($PROTOCOL)" '
+	test_expect_failure "atomic push failed ($PROTOCOL)" '
 		(
 			cd workbench &&
 			git update-ref refs/heads/master $B &&
diff --git a/transport.c b/transport.c
index 13d638d5fe..a07e39564c 100644
--- a/transport.c
+++ b/transport.c
@@ -1248,20 +1248,6 @@ int transport_push(struct repository *r,
 		err = push_had_errors(remote_refs);
 		ret = push_ret | err;
 
-		if ((flags & TRANSPORT_PUSH_ATOMIC) && err) {
-			struct ref *it;
-			for (it = remote_refs; it; it = it->next)
-				switch (it->status) {
-				case REF_STATUS_NONE:
-				case REF_STATUS_UPTODATE:
-				case REF_STATUS_OK:
-					it->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-					break;
-				default:
-					break;
-				}
-		}
-
 		if (!quiet || err)
 			transport_print_push_status(transport->url, remote_refs,
 					verbose | porcelain, porcelain,
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 4/5] transport-helper: mark failure for atomic push
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
                                       ` (3 preceding siblings ...)
  2020-04-17  9:45                     ` [PATCH v4 3/5] send-pack: mark failure of atomic push properly Jiang Xin
@ 2020-04-17  9:45                     ` Jiang Xin
  2020-04-17  9:45                     ` [PATCH v4 5/5] transport-helper: new method reject_atomic_push() Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-17  9:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Commit v2.22.0-1-g3bca1e7f9f (transport-helper: enforce atomic in
push_refs_with_push, 2019-07-11) noticed the incomplete report of
failure of an atomic push for HTTP protocol.  But the implementation
has a flaw that mark all remote references as failure.

Only mark necessary references as failure in `push_refs_with_push()` of
transport-helper.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5541-http-push-smart.sh | 14 +++++++++++---
 t/t5548-push-porcelain.sh  |  5 ++---
 transport-helper.c         | 15 +++++++++++++++
 3 files changed, 28 insertions(+), 6 deletions(-)

diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 2c2c3fb0f5..afc680d5e3 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -177,7 +177,10 @@ test_expect_success 'push (chunked)' '
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
 
-test_expect_failure 'push --atomic also prevents branch creation, reports collateral' '
+## References of remote: atomic1(1)            master(2) collateral(2) other(2)
+## References of local :            atomic2(2) master(1) collateral(3) other(2) collateral1(3) atomic(1)
+## Atomic push         :                       master(1) collateral(3)                         atomic(1)
+test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
 	# Setup upstream repo - empty for now
 	d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
 	git init --bare "$d" &&
@@ -189,7 +192,8 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 	test_commit atomic2 &&
 	git branch collateral &&
 	git branch other &&
-	git push "$up" master collateral other &&
+	git push "$up" atomic1 master collateral other &&
+	git tag -d atomic1 &&
 
 	# collateral is a valid push, but should be failed by atomic push
 	git checkout collateral &&
@@ -224,7 +228,11 @@ test_expect_failure 'push --atomic also prevents branch creation, reports collat
 
 	# the collateral failure refs should be indicated to the user
 	grep "^ ! .*rejected.* atomic -> atomic .*atomic push failed" output &&
-	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output
+	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output &&
+
+	# never report what we do not push
+	! grep "^ ! .*rejected.* atomic1 " output &&
+	! grep "^ ! .*rejected.* other " output
 '
 
 test_expect_success 'push --atomic fails on server-side errors' '
diff --git a/t/t5548-push-porcelain.sh b/t/t5548-push-porcelain.sh
index 9f4b7de74b..1b19b3ef55 100755
--- a/t/t5548-push-porcelain.sh
+++ b/t/t5548-push-porcelain.sh
@@ -136,7 +136,7 @@ run_git_push_porcelain_output_test() {
 	# Refs of upstream : master(A)  bar(B)  baz(A)  next(A)
 	# Refs of workbench: master(B)  bar(A)  baz(A)  next(A)
 	# git-push         : master(B)  bar(A)  NULL    next(A)
-	test_expect_failure "atomic push failed ($PROTOCOL)" '
+	test_expect_success "atomic push failed ($PROTOCOL)" '
 		(
 			cd workbench &&
 			git update-ref refs/heads/master $B &&
@@ -150,10 +150,10 @@ run_git_push_porcelain_output_test() {
 		make_user_friendly_and_stable_output <out >actual &&
 		cat >expect <<-EOF &&
 		To <URL/of/upstream.git>
+		=    refs/heads/next:refs/heads/next    [up to date]
 		!    refs/heads/bar:refs/heads/bar    [rejected] (non-fast-forward)
 		!    (delete):refs/heads/baz    [rejected] (atomic push failed)
 		!    refs/heads/master:refs/heads/master    [rejected] (atomic push failed)
-		!    refs/heads/next:refs/heads/next    [rejected] (atomic push failed)
 		Done
 		EOF
 		test_cmp expect actual &&
@@ -168,7 +168,6 @@ run_git_push_porcelain_output_test() {
 		EOF
 		test_cmp expect actual
 	'
-
 	test_expect_success "prepare pre-receive hook ($PROTOCOL)" '
 		write_script "$upstream/hooks/pre-receive" <<-EOF
 		exit 1
diff --git a/transport-helper.c b/transport-helper.c
index 20a7185ec4..ab3b52eb14 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -894,6 +894,21 @@ static int push_refs_with_push(struct transport *transport,
 		case REF_STATUS_REJECT_STALE:
 		case REF_STATUS_REJECT_ALREADY_EXISTS:
 			if (atomic) {
+				/* Mark other refs as failed */
+				for (ref = remote_refs; ref; ref = ref->next) {
+					if (!ref->peer_ref && !mirror)
+						continue;
+
+					switch (ref->status) {
+					case REF_STATUS_NONE:
+					case REF_STATUS_OK:
+					case REF_STATUS_EXPECTING_REPORT:
+						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+						continue;
+					default:
+						break; /* do nothing */
+					}
+				}
 				string_list_clear(&cas_options, 0);
 				return 0;
 			} else
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 5/5] transport-helper: new method reject_atomic_push()
  2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
                                       ` (4 preceding siblings ...)
  2020-04-17  9:45                     ` [PATCH v4 4/5] transport-helper: mark failure for atomic push Jiang Xin
@ 2020-04-17  9:45                     ` Jiang Xin
  5 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-17  9:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new method in transport-helper to reject all references if any
reference is failed for atomic push.

This method is reused in "send-pack.c" and "transport-helper.c", one for
SSH, git and file protocols, and the other for HTTP protocol.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 send-pack.c        | 29 +++--------------------------
 transport-helper.c | 38 +++++++++++++++++++++++---------------
 transport.h        |  3 +++
 3 files changed, 29 insertions(+), 41 deletions(-)

diff --git a/send-pack.c b/send-pack.c
index efefb687b2..a7c53193c9 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -320,31 +320,6 @@ static int generate_push_cert(struct strbuf *req_buf,
 	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_NONE:
-		case REF_STATUS_OK:
-		case REF_STATUS_EXPECTING_REPORT:
-			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-			continue;
-		default:
-			break; /* do nothing */
-		}
-	}
-	return error("atomic push failed for ref %s. status: %d\n",
-		     failing_ref->name, failing_ref->status);
-}
-
 #define NONCE_LEN_LIMIT 256
 
 static void reject_invalid_nonce(const char *nonce, int len)
@@ -489,7 +464,9 @@ int send_pack(struct send_pack_args *args,
 			if (use_atomic) {
 				strbuf_release(&req_buf);
 				strbuf_release(&cap_buf);
-				atomic_push_failure(args, remote_refs, ref);
+				reject_atomic_push(remote_refs, args->send_mirror);
+				error("atomic push failed for ref %s. status: %d\n",
+				      ref->name, ref->status);
 				return args->porcelain ? 0 : -1;
 			}
 			/* else fallthrough */
diff --git a/transport-helper.c b/transport-helper.c
index ab3b52eb14..a46afcb69d 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -894,21 +894,7 @@ static int push_refs_with_push(struct transport *transport,
 		case REF_STATUS_REJECT_STALE:
 		case REF_STATUS_REJECT_ALREADY_EXISTS:
 			if (atomic) {
-				/* Mark other refs as failed */
-				for (ref = remote_refs; ref; ref = ref->next) {
-					if (!ref->peer_ref && !mirror)
-						continue;
-
-					switch (ref->status) {
-					case REF_STATUS_NONE:
-					case REF_STATUS_OK:
-					case REF_STATUS_EXPECTING_REPORT:
-						ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
-						continue;
-					default:
-						break; /* do nothing */
-					}
-				}
+				reject_atomic_push(remote_refs, mirror);
 				string_list_clear(&cas_options, 0);
 				return 0;
 			} else
@@ -1503,3 +1489,25 @@ int bidirectional_transfer_loop(int input, int output)
 
 	return tloop_spawnwait_tasks(&state);
 }
+
+void reject_atomic_push(struct ref *remote_refs, int mirror_mode)
+{
+	struct ref *ref;
+
+	/* Mark other refs as failed */
+	for (ref = remote_refs; ref; ref = ref->next) {
+		if (!ref->peer_ref && !mirror_mode)
+			continue;
+
+		switch (ref->status) {
+		case REF_STATUS_NONE:
+		case REF_STATUS_OK:
+		case REF_STATUS_EXPECTING_REPORT:
+			ref->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+			continue;
+		default:
+			break; /* do nothing */
+		}
+	}
+	return;
+}
diff --git a/transport.h b/transport.h
index e0131daab9..4298c855be 100644
--- a/transport.h
+++ b/transport.h
@@ -265,4 +265,7 @@ int transport_refs_pushed(struct ref *ref);
 void transport_print_push_status(const char *dest, struct ref *refs,
 		  int verbose, int porcelain, unsigned int *reject_reasons);
 
+/* common method used by transport-helper.c and send-pack.c */
+void reject_atomic_push(struct ref *refs, int mirror_mode);
+
 #endif
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v4 1/5] send-pack: fix inconsistent porcelain output
  2020-04-17  9:45                     ` [PATCH v4 1/5] send-pack: fix inconsistent porcelain output Jiang Xin
@ 2020-04-17 19:51                       ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-04-17 19:51 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/transport.c b/transport.c
> index 1fdc7dac1a..13d638d5fe 100644
> --- a/transport.c
> +++ b/transport.c
> @@ -715,7 +715,15 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re
>  
>  	close(data->fd[1]);
>  	close(data->fd[0]);
> -	ret |= finish_connect(data->conn);
> +	/*
> +	 * Atomic push may abort the connection early and close the pipe,
> +	 * which may cause an error for `finish_connect()`. Ignore this error
> +	 * for atomic git-push.
> +	 */
> +	if (ret || args.atomic)
> +		finish_connect(data->conn);
> +	else
> +		ret = finish_connect(data->conn);

Which means that the return value from this function under the
atomic mode is what we got from an earlier call to send_pack().

Which may have failed, in which case we do want to relay the error
to our caller, or may have succeeded, which is also fine to relay to
our caller.

> The following code at the end of function `send_pack()` indicates that
> `send_pack()` should not return an error if some references are rejected
> in porcelain mode.

Under porcelain mode, if we already saw an error before we get to
the "if porcelain, do not bother checking the remote status", we
do relay the error to our caller.  That happens in the part you
omitted from your quote:

>     int send_pack(...)
>         ... ...

	if (ret < 0)
		return ret;

>
>         if (args->porcelain)
>             return 0;
>
>         for (ref = remote_refs; ref; ref = ref->next) {
>             switch (ref->status) {
>             case REF_STATUS_NONE:
>             case REF_STATUS_UPTODATE:
>             case REF_STATUS_OK:
>                 break;
>             default:
>                 return -1;
>             }
>         }
>         return 0;
>     }

So it is not like we are disabling all errors under the porcelain
mode.

> diff --git a/send-pack.c b/send-pack.c
> index 0407841ae8..1835cd5582 100644
> --- a/send-pack.c
> +++ b/send-pack.c
> @@ -489,7 +487,8 @@ int send_pack(struct send_pack_args *args,
>  			if (use_atomic) {
>  				strbuf_release(&req_buf);
>  				strbuf_release(&cap_buf);
> -				return atomic_push_failure(args, remote_refs, ref);
> +				atomic_push_failure(args, remote_refs, ref);
> +				return args->porcelain ? 0 : -1;
>  			}

And this is in line with the way how rejected pushes are handled, I guess.

Sounds a bit convoluted but correct ;-)

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v13 0/8] New proc-receive hook for centralized workflow
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 1/8] transport: not report a non-head push as a branch Jiang Xin
                                       ` (7 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Note: This patches series is based on topic branch `jx/atomic-push`,
      for there are overlap changes between these two topics.

## Changes since v12

1. test: Remove t5412, and move HTTP test cases into t5411.

2. test: Split test cases into multiple files as `t/t5411/test-*.sh`.

3. test: Add test cases for porcelain `git-push` output for
   `pro-receive` hook.  See files 't/t5411/test-*--porcelain.sh'.

4. New patch 5/8 to feed proper commands to `post-receive` hook.

5. Use flag `PACKET_READ_GENTLE_ON_EOF` instead of
   `PACKET_READ_DIE_ON_ERR_PACKET` for `packet_reader_init()`
   when reading result from the 'proc-receive' hook.
   So 'receive-pack' won't die with the `proc-receive` hook.

6.  Add two new options (`--die-version` and `--die-readline`) for
   't/helper/test-proc-receive.c', so we can test unexpected crash of
   'proc-receive' hook.

7. Apply the "SQUASH???" commit.

## Range-diff v12...v13

1:  77a731c151 < -:  ---------- transport: not report a non-head push as a branch
-:  ---------- > 1:  312ee17e84 transport: not report a non-head push as a branch
2:  4c4f7cffff = 2:  e86a3c5a4e connect: export parse_feature_value()
3:  b76900cb0f ! 3:  5adc60e9d1 receive-pack: add new proc-receive hook
    @@ Makefile: TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
      TEST_BUILTINS_OBJS += test-read-cache.o
     
      ## builtin/receive-pack.c ##
    -@@
    - #include "remote.h"
    - #include "connect.h"
    - #include "string-list.h"
    -+#include "string.h"
    - #include "sha1-array.h"
    - #include "connected.h"
    - #include "argv-array.h"
     @@ builtin/receive-pack.c: static void write_head_info(void)
      	packet_flush(1);
      }
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +	for (;;) {
     +		struct object_id old_oid, new_oid;
    -+		char *status;
    ++		const char *status;
     +		char *refname;
     +		char *remains = NULL;
     +
     +		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
     +			break;
    -+		status = (char *)reader->line;
    ++		status = reader->line;
     +		refname = strchr(status, ' ');
     +		if (refname) {
     +			*refname++ = '\0';
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
     +			hint->run_proc_receive = 0;
     +		} else if (!strcmp("alt", status)) {
    -+			char *alt_ref;
    ++			char *alt_ref = NULL;
    ++			char *p1, *p2;
     +
    -+			alt_ref = remains;
    -+			remains = strchr(alt_ref, ' ');
    -+			if (remains)
    -+				*remains++ = '\0';
    ++			if (!remains) {
    ++				strbuf_addf(errmsg, "proc-receive expected 'alt <ref> <alt-ref> ...', got 'alt %s'\n",
    ++					    refname);
    ++				continue;
    ++			}
    ++			p1 = strchr(remains, '=');
    ++			p2 = strchr(remains, ' ');
    ++			if (!p1) {
    ++				/*
    ++				 * "alt <ref> <alt-ref>", or
    ++				 * "alt <ref> <alt-ref> forced-update"
    ++				 */
    ++				alt_ref = remains;
    ++				remains = p2;
    ++				if (remains)
    ++					*remains++ = '\0';
    ++			} else if (!p2) {
    ++				/* "alt <ref> old-oid=<oid>" */
    ++				alt_ref = NULL;
    ++			} else if (p1 > p2) {
    ++				/* "alt <ref> <alt-ref> old-oid=<oid>" */
    ++				alt_ref = remains;
    ++				remains = p2;
    ++				if (remains)
    ++					*remains++ = '\0';
    ++			} else {
    ++				/* "alt <ref> old-oid=<oid1> new-oid=<oid2>" */
    ++				alt_ref = NULL;
    ++			}
     +
     +			if (hint->extended_status)
     +				strbuf_addstr(&extended_status, hint->extended_status);
     +
    -+			strbuf_addf(&extended_status, "%sref=%s",
    -+				    extended_status.len > 0 ? " ": "",
    -+				    alt_ref);
    ++			/*
    ++			 * Already received an "ok <ref>" response, add an
    ++			 * additional "ref=<ref>" field.
    ++			 */
    ++			if (extended_status.len == 0 &&
    ++			    hint->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)
    ++				strbuf_addf(&extended_status, "ref=%s", refname);
    ++
    ++			if (alt_ref)
    ++				strbuf_addf(&extended_status, "%sref=%s",
    ++					    extended_status.len > 0 ? " ": "",
    ++					    alt_ref);
    ++			else if (extended_status.len > 0)
    ++				/*
    ++				 * Start new extended status for refname, if it
    ++				 * is not the first report for this reference.
    ++				 */
    ++				strbuf_addf(&extended_status, " ref=%s", refname);
     +
     +			if (remains) {
     +				const char *val;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	/* Version negotiaton */
     +	packet_reader_init(&reader, proc.out, NULL, 0,
     +			   PACKET_READ_CHOMP_NEWLINE |
    -+			   PACKET_READ_DIE_ON_ERR_PACKET);
    ++			   PACKET_READ_GENTLE_ON_EOF);
     +	if (use_atomic)
     +		strbuf_addstr(&cap, " atomic");
     +	if (use_push_options)
    @@ t/helper/test-proc-receive.c (new)
     +	NULL
     +};
     +
    -+static int version = 1;
    -+static int verbose = 0;
    ++static int die_version = 0;
    ++static int die_readline = 0;
     +static int no_push_options = 0;
     +static int use_atomic = 0;
     +static int use_push_options = 0;
    ++static int verbose = 0;
    ++static int version = 1;
     +static struct string_list returns = STRING_LIST_INIT_NODUP;
     +
     +struct command {
    @@ t/helper/test-proc-receive.c (new)
     +		}
     +	}
     +
    -+	if (server_version != 1)
    ++	if (server_version != 1 || die_version)
     +		die("bad protocol version: %d", server_version);
     +
     +	packet_write_fmt(1, "version=%d%c%s\n",
    @@ t/helper/test-proc-receive.c (new)
     +		if (parse_oid_hex(reader->line, &old_oid, &p) ||
     +		    *p++ != ' ' ||
     +		    parse_oid_hex(p, &new_oid, &p) ||
    -+		    *p++ != ' ')
    ++		    *p++ != ' ' ||
    ++		    die_readline)
     +			die("protocol error: expected 'old new ref', got '%s'",
     +			    reader->line);
     +		refname = p;
    @@ t/helper/test-proc-receive.c (new)
     +	struct option options[] = {
     +		OPT_BOOL(0, "no-push-options", &no_push_options,
     +			 "disable push options"),
    ++		OPT_BOOL(0, "die-version", &die_version,
    ++			 "die during version negotiation"),
    ++		OPT_BOOL(0, "die-readline", &die_readline,
    ++			 "die when readline"),
     +		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
     +				"return of results"),
     +		OPT__VERBOSE(&verbose, "be verbose"),
    @@ t/helper/test-proc-receive.c (new)
     +	}
     +
     +	if (returns.nr)
    -+		for_each_string_list_item(item, &returns) {
    -+			char *p;
    -+
    -+			p = strstr(item->string, "\\0");
    -+			if (p) {
    -+				*p = '\0';
    -+				p += 2;
    -+				packet_write_fmt(1, "%s%c%s\n", item->string, '\0', p);
    -+			} else {
    -+				packet_write_fmt(1, "%s\n", item->string);
    -+			}
    -+		}
    ++		for_each_string_list_item(item, &returns)
    ++			packet_write_fmt(1, "%s\n", item->string);
     +	packet_flush(1);
     +	sigchain_pop(SIGPIPE);
     +
    @@ t/helper/test-tool.h: int cmd__parse_pathspec_file(int argc, const char** argv);
      int cmd__reach(int argc, const char **argv);
      int cmd__read_cache(int argc, const char **argv);
     
    - ## t/t5411/common-functions.sh ##
    -@@ t/t5411/common-functions.sh: create_commits_in () {
    - # Format the output of git-push, git-show-ref and other commands to make a
    - # user-friendly and stable text.  We can easily prepare the expect text
    - # without having to worry about future changes of the commit ID and spaces
    --# of the output.  We also replce single quotes with double quotes, because
    --# it is boring to prepare unquoted single quotes in expect text.
    -+# of the output.  Single quotes are replaced with double quotes, because
    -+# it is boring to prepare unquoted single quotes in expect text.  We also
    -+# remove some locale error messages, which break test if we turn on
    -+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
    -+# on plumbing commands.
    - make_user_friendly_and_stable_output_common () {
    - 	sed \
    - 		-e "s/  *\$//" \
    -@@ t/t5411/common-functions.sh: make_user_friendly_and_stable_output_common () {
    - 		-e "s/$TAG/<TAG-v123>/g" \
    - 		-e "s/$ZERO_OID/<ZERO-OID>/g" \
    - 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
    --		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g"
    -+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
    -+		-e "/^error: / d"
    - }
    -
    - ## t/t5411/common-test-cases.sh ##
    -@@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
    - 	EOF
    - 	test_cmp expect actual
    - '
    -+
    -+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
    -+# Refs of workbench: master(A)  tags/v123
    -+test_expect_success "cleanup" '
    -+	(
    -+		cd "$upstream" &&
    -+		git update-ref -d refs/review/master/topic &&
    -+		git update-ref -d refs/tags/v123 &&
    -+		git update-ref -d refs/heads/a/b/c
    -+	)
    -+'
    -+
    + ## t/t5411/test-0011-no-hook-error.sh (new) ##
    +@@
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       next(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: no hook, fail to push special ref" '
    ++test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:next \
     +		HEAD:refs/for/master/topic \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +
     +# Refs of upstream : master(A)             next(A)
     +# Refs of workbench: master(A)  tags/v123
    -+test_expect_success "cleanup" '
    ++test_expect_success "cleanup ($PROTOCOL)" '
     +	git -C "$upstream" update-ref -d refs/heads/next
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: no hook, fail all for atomic push" '
    ++test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
     +	test_must_fail git -C workbench push --atomic origin \
     +		$B:master \
     +		HEAD:next \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0012-no-hook-error--porcelain.sh (new) ##
    +@@
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       next(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: no hook, fail to push special ref (--porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		HEAD:next \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: error: cannot find hook "proc-receive"
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/heads/next    [new branch]
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	<COMMIT-A> refs/heads/next
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref -d refs/heads/next
     +'
     +
    -+test_expect_success "setup proc-receive hook (unknown version)" '
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: no hook, all failed for atomic push (--porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain --atomic origin \
    ++		$B:master \
    ++		HEAD:next \
    ++		HEAD:refs/for/master/topic >out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: error: cannot find hook "proc-receive"
    ++	To <URL/of/upstream.git>
    ++	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
    ++	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0013-bad-protocol.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v --version 2
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: bad protocol (unknown version)" '
    ++test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +
     +	# Check status report for git-push
     +	sed -n \
    -+		-e "/^To / { s/   */ /g; p; }" \
    -+		-e "/^ / { s/   */ /g; p; }" \
    ++		-e "/^To / { p; n; p; }" \
     +		<actual >actual-report &&
     +	cat >expect <<-EOF &&
     +	To <URL/of/upstream.git>
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	# Check error message from "receive-pack", but ignore unstable fatal error
     +	# message ("remote: fatal: the remote end hung up unexpectedly") which
     +	# is different from the remote HTTP server with different locale settings.
    -+	sed -n -e "/^remote: error:/ { s/   */ /g; p; }" \
    -+		<actual >actual-error &&
    ++	grep "^remote: error:" <actual >actual-error &&
     +	cat >expect <<-EOF &&
     +	remote: error: proc-receive version "2" is not supported
     +	EOF
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (no report)" '
    ++test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v --die-version
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: fatal: bad protocol version: 1
    ++	remote: error: proc-receive version "0" is not supported
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
    ++	EOF
    ++	test_cmp expect actual &&
    ++
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v --die-readline
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: fatal: protocol error: expected "old new ref", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
    ++	EOF
    ++	test_cmp expect actual &&
    ++
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       next(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: bad protocol (no report)" '
    ++test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/heads/next \
     +		HEAD:refs/for/master/topic >out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +
     +# Refs of upstream : master(A)             next(A)
     +# Refs of workbench: master(A)  tags/v123
    -+test_expect_success "cleanup" '
    ++test_expect_success "cleanup ($PROTOCOL)" '
     +	git -C "$upstream" update-ref -d refs/heads/next
     +
     +'
     +
    -+test_expect_success "setup proc-receive hook (no ref)" '
    ++test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: bad protocol (no ref)" '
    ++test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic\
     +		>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (unknown status)" '
    ++test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: bad protocol (unknown status)" '
    ++test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
     +	test_must_fail git -C workbench push origin \
     +			HEAD:refs/for/master/topic \
     +			>out 2>&1 &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	EOF
     +	test_cmp expect actual
     +'
    -+
    -+test_expect_success "setup proc-receive hook (ng)" '
    +
    + ## t/t5411/test-0014-bad-protocol--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+	test-tool proc-receive -v \
    -+		-r "ng refs/for/master/topic"
    ++	test-tool proc-receive -v --version 2
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: fail to update (ng)" '
    -+	test_must_fail git -C workbench push origin \
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: bad protocol (unknown version, --porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
    ++
    ++	# Check status report for git-push
    ++	sed -n \
    ++		-e "/^To / { p; n; p; n; p; }" \
    ++		<actual >actual-report &&
     +	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> ng refs/for/master/topic
     +	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
    ++	Done
     +	EOF
    -+	test_cmp expect actual &&
    ++	test_cmp expect actual-report &&
    ++
    ++	# Check error message from "receive-pack", but ignore unstable fatal error
    ++	# message ("remote: fatal: the remote end hung up unexpectedly") which
    ++	# is different from the remote HTTP server with different locale settings.
    ++	grep "^remote: error:" <actual >actual-error &&
    ++	cat >expect <<-EOF &&
    ++	remote: error: proc-receive version "2" is not supported
    ++	EOF
    ++	test_cmp expect actual-error &&
    ++
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (ng message)" '
    ++test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
    -+	test-tool proc-receive -v \
    -+		-r "ng refs/for/master/topic error msg"
    ++	test-tool proc-receive -v
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: fail to update (ng message)" '
    -+	test_must_fail git -C workbench push origin \
    -+		HEAD:refs/for/master/topic \
    -+		>out 2>&1 &&
    ++# git push         :                       next(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: bad protocol (no report, --porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		HEAD:refs/heads/next \
    ++		HEAD:refs/for/master/topic >out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> ng refs/for/master/topic error msg
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
    ++	*    HEAD:refs/heads/next    [new branch]
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
    ++	Done
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    ++	<COMMIT-A> refs/heads/next
     +	EOF
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (unexpected ref)" '
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref -d refs/heads/next
    ++
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ok refs/heads/master"
    ++		-r "ok"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push         : (B)                   refs/for/master/topic
    -+test_expect_success "proc-receive: report on unexpected ref" '
    -+	test_must_fail git -C workbench push origin \
    -+		$B:refs/heads/master \
    -+		HEAD:refs/for/master/topic \
    ++# git push         :                       refs/for/master/topic
    ++test_expect_success "proc-receive: bad protocol (no ref, --porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic\
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    -+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> ok refs/heads/master
    -+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
    -+	remote: # post-receive hook
    -+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: proc-receive> ok
    ++	remote: error: proc-receive expected "<status> <ref> ...", got "ok"
     +	To <URL/of/upstream.git>
    -+	 <OID-A>..<OID-B> <COMMIT-B> -> master
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
    ++	Done
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	<COMMIT-B> refs/heads/master
    ++	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
     +'
     +
    -+# Refs of upstream : master(B)
    -+# Refs of workbench: master(A)  tags/v123
    -+test_expect_success "cleanup" '
    -+	git -C "$upstream" update-ref refs/heads/master $A
    -+'
    -+
    -+test_expect_success "setup proc-receive hook (ok)" '
    ++test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ok refs/for/master/topic"
    ++		-r "xx refs/for/master/topic"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: ok" '
    -+	git -C workbench push origin \
    -+		HEAD:refs/for/master/topic \
    -+		>out 2>&1 &&
    ++test_expect_success "proc-receive: bad protocol (unknown status, --porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++			HEAD:refs/for/master/topic \
    ++			>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> ok refs/for/master/topic
    -+	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> xx refs/for/master/topic
    ++	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
     +	To <URL/of/upstream.git>
    -+	 * [new reference] HEAD -> refs/for/master/topic
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
    ++	Done
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0020-report-ng.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ng refs/for/master/topic"
    ++	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/a/b/c/my/topic
    -+test_expect_success "proc-receive: failed to report status" '
    ++# git push         :                       refs/for/master/topic
    ++test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
     +	test_must_fail git -C workbench push origin \
    -+		HEAD:refs/for/a/b/c/my/topic \
    ++		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
    -+	remote: proc-receive> ok refs/for/master/topic
    -+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ng refs/for/master/topic
     +	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+# Refs of upstream : master(A)
    -+# Refs of workbench: master(A)  tags/v123
    -+# git push -o ...  :                       refs/for/master/topic
    -+test_expect_success "proc-receive: not support push options" '
    -+	test_must_fail git -C workbench push \
    ++test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ng refs/for/master/topic error msg"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
    ++test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ng refs/for/master/topic error msg
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0021-report-ng--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ng refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
    ++test_expect_success "proc-receive: fail to update (ng, no message, --porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ng refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ng refs/for/master/topic error msg"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
    ++test_expect_success "proc-receive: fail to update (ng, with message, --porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ng refs/for/master/topic error msg
    ++	To <URL/of/upstream.git>
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0022-report-unexpect-ref.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/heads/master"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         : (B)                   refs/for/master/topic
    ++test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
    ++	test_must_fail git -C workbench push origin \
    ++		$B:refs/heads/master \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/heads/master
    ++	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
    ++	remote: # post-receive hook
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	To <URL/of/upstream.git>
    ++	 <OID-A>..<OID-B> <COMMIT-B> -> master
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-B> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(B)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref refs/heads/master $A
    ++'
    +
    + ## t/t5411/test-0023-report-unexpect-ref--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/heads/master"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         : (B)                   refs/for/master/topic
    ++test_expect_success "proc-receive: report unexpected ref (--porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		$B:refs/heads/master \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/heads/master
    ++	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
    ++	remote: # post-receive hook
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	To <URL/of/upstream.git>
    ++	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-B> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(B)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref refs/heads/master $A
    ++'
    +
    + ## t/t5411/test-0024-report-unknown-ref.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/a/b/c/my/topic
    ++test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/a/b/c/my/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0025-report-unknown-ref--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/a/b/c/my/topic
    ++test_expect_success "proc-receive: report unknown reference (--porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/a/b/c/my/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0026-push-options.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
    ++	git -C "$upstream" config receive.advertisePushOptions false &&
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push -o ...  :                       refs/for/master/topic
    ++test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
    ++	test_must_fail git -C workbench push \
     +		-o issue=123 \
     +		-o reviewer=user1 \
     +		origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
    -+	test_i18ngrep "fatal: the receiving end does not support push options" \
    -+		actual &&
    ++	test_i18ngrep "fatal: the receiving end does not support push options" \
    ++		actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "enable push options ($PROTOCOL)" '
    ++	git -C "$upstream" config receive.advertisePushOptions true
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push -o ...  :                       next(A)  refs/for/master/topic
    ++test_expect_success "proc-receive: push with options ($PROTOCOL)" '
    ++	git -C workbench push \
    ++		--atomic \
    ++		-o issue=123 \
    ++		-o reviewer=user1 \
    ++		origin \
    ++		HEAD:refs/heads/next \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive: atomic push_options
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive< issue=123
    ++	remote: proc-receive< reviewer=user1
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new branch] HEAD -> next
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	<COMMIT-A> refs/heads/next
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref -d refs/heads/next
    ++'
    +
    + ## t/t5411/test-0027-push-options--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
    ++	git -C "$upstream" config receive.advertisePushOptions false &&
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push -o ...  :                       refs/for/master/topic
    ++test_expect_success "proc-receive: not support push options (--porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push \
    ++		--porcelain \
    ++		-o issue=123 \
    ++		-o reviewer=user1 \
    ++		origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	test_i18ngrep "fatal: the receiving end does not support push options" \
    ++		actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "enable push options ($PROTOCOL)" '
    ++	git -C "$upstream" config receive.advertisePushOptions true
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push -o ...  :                       next(A)  refs/for/master/topic
    ++test_expect_success "proc-receive: push with options (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push \
    ++		--porcelain \
    ++		--atomic \
    ++		-o issue=123 \
    ++		-o reviewer=user1 \
    ++		origin \
    ++		HEAD:refs/heads/next \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive: atomic push_options
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive< issue=123
    ++	remote: proc-receive< reviewer=user1
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/heads/next    [new branch]
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	<COMMIT-A> refs/heads/next
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(A)             next(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref -d refs/heads/next
    ++'
    +
    + ## t/t5411/test-0030-report-ok.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
    ++test_expect_success "proc-receive: ok ($PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0031-report-ok--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic
    ++test_expect_success "proc-receive: ok (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0032-report-alt.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (alt <ref>, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref>, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic
    ++	remote: error: proc-receive expected "alt <ref> <alt-ref> ...", got "alt refs/for/master/topic"
    ++	To <URL/of/upstream.git>
    ++	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> <alt-ref>, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/pull/123/head"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, $PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> forced-update, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/pull/123/head forced-update"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update, $PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> old-oid=X, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, $PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> old-oid=X, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic old-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, $PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> old-oid=X new-oid=Y, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic old-oid=$A new-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, $PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (with multiple alt reports, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/a/b/c/topic" \
    ++		-r "alt refs/for/next/topic refs/pull/123/head" \
    ++		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: with multiple alt reports ($PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/next/topic \
    ++		HEAD:refs/for/a/b/c/topic \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/a/b/c/topic
    ++	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/next/topic
    ++	 * [new reference] HEAD -> refs/for/a/b/c/topic
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0033-report-alt--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (alt <ref>, --porcelain, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref>, --porcelain, $PROTOCOL)" '
    ++	test_must_fail git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic
    ++	remote: error: proc-receive expected "alt <ref> <alt-ref> ...", got "alt refs/for/master/topic"
    ++	To <URL/of/upstream.git>
    ++	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> <alt-ref>, --porcelain, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/pull/123/head"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, --porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> forced-update, --porcelain, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/pull/123/head forced-update"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update, --porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic old-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (alt <ref> old-oid=X new-oid=Y, --porcelain, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic old-oid=$A new-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, --porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (with multiple alt reports, --porcelain, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/a/b/c/topic" \
    ++		-r "alt refs/for/next/topic refs/pull/123/head" \
    ++		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: with multiple alt reports (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/next/topic \
    ++		HEAD:refs/for/a/b/c/topic \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/a/b/c/topic
    ++	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/next/topic    [new reference]
    ++	*    HEAD:refs/for/a/b/c/topic    [new reference]
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0034-report-ft.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ft refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(B)
    ++test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		$B:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: proc-receive> ft refs/for/master/topic
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] <COMMIT-B> -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-B> refs/for/master/topic
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(A)             refs/for/master/topic(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref -d refs/for/master/topic
    ++'
    +
    + ## t/t5411/test-0035-report-ft--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ft refs/for/master/topic"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(B)
    ++test_expect_success "proc-receive: fall throught, let receive-pack to execute (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		$B:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: proc-receive> ft refs/for/master/topic
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    <COMMIT-B>:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-B> refs/for/master/topic
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(A)             refs/for/master/topic(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref -d refs/for/master/topic
    ++'
    +
    + ## t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 1st resp, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    ++		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    ++		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$A new-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st resp ($PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "enable push options" '
    -+	git -C "$upstream" config receive.advertisePushOptions true
    ++test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 2nd resp, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    ++		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    ++		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$B new-oid=$A forced-update"
    ++	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push -o ...  :                       next(A)  refs/for/master/topic
    -+test_expect_success "proc-receive: push with options" '
    -+	git -C workbench push \
    -+		--atomic \
    -+		-o issue=123 \
    -+		-o reviewer=user1 \
    -+		origin \
    -+		HEAD:refs/heads/next \
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd resp ($PROTOCOL)" '
    ++	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
    -+	remote: proc-receive: atomic push_options
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive< issue=123
    -+	remote: proc-receive< reviewer=user1
    -+	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
     +	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    -+	 * [new branch] HEAD -> next
     +	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    -+	<COMMIT-A> refs/heads/next
     +	EOF
     +	test_cmp expect actual
     +'
     +
    -+# Refs of upstream : master(A)             next(A)
    ++test_expect_success "setup proc-receive hook (ok and alt for the same ref, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "alt refs/for/master/topic refs/changes/24/124/1 new-oid=$B old-oid=$A"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+test_expect_success "cleanup" '
    -+	git -C "$upstream" update-ref -d refs/heads/next
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report ok and alt for the same ref ($PROTOCOL)" '
    ++	git -C workbench push origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	 * [new reference] HEAD -> refs/for/master/topic
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (ft)" '
    ++test_expect_success "setup proc-receive hook (multiple responses, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ft refs/for/master/topic"
    ++		-r "alt refs/for/master/topic refs/changes/23/123/1" \
    ++		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/master/topic(B)
    -+test_expect_success "proc-receive: fall throught, let receive-pack to execute" '
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report multiple response ($PROTOCOL)" '
     +	git -C workbench push origin \
    -+		$B:refs/for/master/topic \
    ++		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    -+	remote: proc-receive> ft refs/for/master/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
     +	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    -+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/for/master/topic
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    -+	<COMMIT-B> refs/for/master/topic
     +	<COMMIT-A> refs/heads/master
     +	EOF
     +	test_cmp expect actual
    ++'
    +
    + ## t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 1st resp, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    ++		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    ++		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$A new-oid=$B"
    ++	EOF
     +'
     +
    -+# Refs of upstream : master(A)             refs/for/master/topic(A)
    ++# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+test_expect_success "cleanup" '
    -+	git -C "$upstream" update-ref -d refs/for/master/topic
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st resp (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (with extended status)" '
    ++test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 2nd resp, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ok refs/for/a/b/c/topic" \
    -+		-r "alt refs/for/next/topic refs/pull/123/head" \
    -+		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
    ++		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    ++		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    ++		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$B new-oid=$A forced-update"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report with extended status" '
    -+	git -C workbench push origin \
    -+		HEAD:refs/for/next/topic \
    -+		HEAD:refs/for/a/b/c/topic \
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd resp (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> ok refs/for/a/b/c/topic
    -+	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    ++	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
     +	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    -+	 * [new reference] HEAD -> refs/for/next/topic
    -+	 * [new reference] HEAD -> refs/for/a/b/c/topic
    -+	 * [new reference] HEAD -> refs/for/master/topic
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success "setup proc-receive hook (ok and alt for the same ref, $PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "alt refs/for/master/topic refs/changes/24/124/1 new-oid=$B old-oid=$A"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         :                       refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report ok and alt for the same ref (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (multiple responses)" '
    ++test_expect_success "setup proc-receive hook (multiple responses, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report multiple response" '
    -+	git -C workbench push origin \
    ++test_expect_success "proc-receive: report multiple response (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    -+	 * [new reference] HEAD -> refs/for/master/topic
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	EOF
     +	test_cmp expect actual
     +'
    -+
    -+test_expect_success "setup proc-receive hook" '
    +
    + ## t/t5411/test-0038-report-mixed-refs.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook ($PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report update of mixed refs" '
    ++test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/heads/bar \
    @@ t/t5411/common-test-cases.sh: test_expect_success "normal git-push command" '
     +	EOF
     +	test_cmp expect actual
     +'
    ++
    ++# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	(
    ++		cd "$upstream" &&
    ++		git update-ref refs/heads/master $A &&
    ++		git update-ref -d refs/heads/foo &&
    ++		git update-ref -d refs/heads/bar &&
    ++		git update-ref -d refs/heads/baz
    ++	)
    ++
    ++'
    +
    + ## t/t5411/test-0039-report-mixed-refs--porcelain.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook ($PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/next/topic" \
    ++		-r "alt refs/for/master/topic refs/for/master/topic old-oid=$A new-oid=$B"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
    ++test_expect_success "proc-receive: report update of mixed refs (--porcelain, $PROTOCOL)" '
    ++	git -C workbench push --porcelain origin \
    ++		$B:refs/heads/master \
    ++		HEAD:refs/heads/bar \
    ++		HEAD:refs/heads/baz \
    ++		HEAD:refs/for/next/topic \
    ++		HEAD:refs/heads/foo \
    ++		HEAD:refs/for/master/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive> alt refs/for/master/topic refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: # post-receive hook
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	To <URL/of/upstream.git>
    ++	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
    ++	*    HEAD:refs/heads/bar    [new branch]
    ++	*    HEAD:refs/heads/baz    [new branch]
    ++	*    HEAD:refs/for/next/topic    [new reference]
    ++	*    HEAD:refs/heads/foo    [new branch]
    ++	*    HEAD:refs/for/master/topic    [new reference]
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/heads/bar
    ++	<COMMIT-A> refs/heads/baz
    ++	<COMMIT-A> refs/heads/foo
    ++	<COMMIT-B> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	(
    ++		cd "$upstream" &&
    ++		git update-ref refs/heads/master $A &&
    ++		git update-ref -d refs/heads/foo &&
    ++		git update-ref -d refs/heads/bar &&
    ++		git update-ref -d refs/heads/baz
    ++	)
    ++
    ++'
4:  d20702291b ! 4:  f03038c82f send-pack: extension for client-side status report
    @@ remote.c: void apply_push_cas(struct push_cas_option *cas,
     +void update_ref_from_remote_status(struct ref *ref)
     +{
     +	char *val;
    -+	char c;
     +	int len;
     +
     +	if (!ref->remote_status)
     +		return;
     +
     +	val = (char *)parse_feature_value(ref->remote_status, "old-oid", &len);
    -+	if (val && len) {
    -+		c = *(val+len);
    -+		*(val+len) = '\0';
    ++	if (val && len)
     +		get_oid_hex(val, &ref->old_oid);
    -+		*(val+len) = c;
    -+	}
    ++
     +	val = (char *)parse_feature_value(ref->remote_status, "new-oid", &len);
    -+	if (val && len) {
    -+		c = *(val+len);
    -+		*(val+len) = '\0';
    ++	if (val && len)
     +		get_oid_hex(val, &ref->new_oid);
    -+		*(val+len) = c;
    -+	}
    ++
     +	if (parse_feature_request(ref->remote_status, "forced-update"))
     +		ref->forced_update = 1;
     +}
    @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref
      		/* first try searching at our hint, falling back to all refs */
      		if (hint)
     @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref *refs)
    + 			hint->status = REF_STATUS_OK;
    + 		else
      			hint->status = REF_STATUS_REMOTE_REJECT;
    - 			ret = -1;
    - 		}
     -		hint->remote_status = xstrdup_or_null(msg);
     +		if (msg)
     +			hint->remote_status = xstrdup(msg);
     +		else if (extended_status)
    -+			hint->remote_status = xstrdup(extended_status);
    ++			hint->remote_status = xstrdup_or_null(extended_status);
      		/* start our next search from the next ref */
      		hint = hint->next;
      	}
     
    - ## t/t5411/common-test-cases.sh ##
    -@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report with extended status" '
    + ## t/t5411/test-0032-report-alt.sh ##
    +@@ t/t5411/test-0032-report-alt.sh: test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, $PROTOCOL)"
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/pull/123/head
    + 	EOF
    + 	test_cmp expect actual
    + '
    +@@ t/t5411/test-0032-report-alt.sh: test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/pull/123/head
    + 	EOF
    + 	test_cmp expect actual
    + '
    +@@ t/t5411/test-0032-report-alt.sh: test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, $P
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
    + 	EOF
    + 	test_cmp expect actual
    + '
    +@@ t/t5411/test-0032-report-alt.sh: test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, $PROTOCOL)"
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
    + 	EOF
    + 	test_cmp expect actual
    + '
    +@@ t/t5411/test-0032-report-alt.sh: test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, $P
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
    + 	EOF
    + 	test_cmp expect actual
    + '
    +@@ t/t5411/test-0032-report-alt.sh: test_expect_success "proc-receive: with multiple alt reports ($PROTOCOL)" '
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
      	To <URL/of/upstream.git>
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report with ext
     +	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
      	EOF
      	test_cmp expect actual &&
    + 
    +
    + ## t/t5411/test-0033-report-alt--porcelain.sh ##
    +@@ t/t5411/test-0033-report-alt--porcelain.sh: test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, --porcelain,
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	*    HEAD:refs/pull/123/head    [new reference]
    + 	Done
    + 	EOF
    + 	test_cmp expect actual
    +@@ t/t5411/test-0033-report-alt--porcelain.sh: test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	*    HEAD:refs/pull/123/head    [new reference]
    + 	Done
    + 	EOF
    + 	test_cmp expect actual
    +@@ t/t5411/test-0033-report-alt--porcelain.sh: test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, --
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
    + 	Done
    + 	EOF
    + 	test_cmp expect actual
    +@@ t/t5411/test-0033-report-alt--porcelain.sh: test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, --porcelain,
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
    + 	Done
    + 	EOF
    + 	test_cmp expect actual
    +@@ t/t5411/test-0033-report-alt--porcelain.sh: test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, --
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
    + 	Done
    + 	EOF
    + 	test_cmp expect actual
    +@@ t/t5411/test-0033-report-alt--porcelain.sh: test_expect_success "proc-receive: with multiple alt reports (--porcelain, $PROT
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/next/topic    [new reference]
    ++	*    HEAD:refs/pull/123/head    [new reference]
    + 	*    HEAD:refs/for/a/b/c/topic    [new reference]
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
    + 	Done
    + 	EOF
    + 	test_cmp expect actual &&
    +
    + ## t/t5411/test-0036-report-multi-alt-for-one-ref.sh ##
    +@@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh: test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st r
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/changes/24/124/1
    ++	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
    + 	EOF
    + 	test_cmp expect actual &&
      	git -C "$upstream" show-ref >out &&
    -@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report multiple response" '
    +@@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh: test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd r
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	 * [new reference] HEAD -> refs/for/master/topic
    ++	 * [new reference] HEAD -> refs/changes/24/124/1
    ++	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
    ++	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
    + 	EOF
    + 	test_cmp expect actual &&
    + 	git -C "$upstream" show-ref >out &&
    +@@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh: test_expect_success "proc-receive: report ok and alt for the same ref ($PROTOCOL
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    + 	 * [new reference] HEAD -> refs/for/master/topic
    ++	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/1
    + 	EOF
    + 	test_cmp expect actual &&
    + 	git -C "$upstream" show-ref >out &&
    +@@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh: test_expect_success "proc-receive: report multiple response ($PROTOCOL)" '
      	remote: # post-receive hook
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
      	To <URL/of/upstream.git>
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report multiple
      	EOF
      	test_cmp expect actual &&
      	git -C "$upstream" show-ref >out &&
    -@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update of mixed refs" '
    +
    + ## t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh ##
    +@@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh: test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st r
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
    ++	*    HEAD:refs/changes/24/124/1    [new reference]
    ++	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
    + 	Done
    + 	EOF
    + 	test_cmp expect actual &&
    +@@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh: test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd r
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	*    HEAD:refs/changes/24/124/1    [new reference]
    ++	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
    ++	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
    + 	Done
    + 	EOF
    + 	test_cmp expect actual &&
    +@@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh: test_expect_success "proc-receive: report ok and alt for the same ref (--porcela
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    + 	*    HEAD:refs/for/master/topic    [new reference]
    ++	     HEAD:refs/changes/24/124/1    <OID-A>..<OID-B>
    + 	Done
    + 	EOF
    + 	test_cmp expect actual &&
    +@@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh: test_expect_success "proc-receive: report multiple response (--porcelain, $PROTO
    + 	remote: # post-receive hook
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    + 	To <URL/of/upstream.git>
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	*    HEAD:refs/changes/23/123/1    [new reference]
    ++	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
    + 	Done
    + 	EOF
    + 	test_cmp expect actual &&
    +
    + ## t/t5411/test-0038-report-mixed-refs.sh ##
    +@@ t/t5411/test-0038-report-mixed-refs.sh: test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
      	 * [new branch] HEAD -> baz
      	 * [new reference] HEAD -> refs/for/next/topic
      	 * [new branch] HEAD -> foo
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update o
      	test_cmp expect actual &&
      	git -C "$upstream" show-ref >out &&
     
    + ## t/t5411/test-0039-report-mixed-refs--porcelain.sh ##
    +@@ t/t5411/test-0039-report-mixed-refs--porcelain.sh: test_expect_success "proc-receive: report update of mixed refs (--porcelain, $PR
    + 	*    HEAD:refs/heads/baz    [new branch]
    + 	*    HEAD:refs/for/next/topic    [new reference]
    + 	*    HEAD:refs/heads/foo    [new branch]
    +-	*    HEAD:refs/for/master/topic    [new reference]
    ++	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
    + 	Done
    + 	EOF
    + 	test_cmp expect actual &&
    +
      ## transport-helper.c ##
     @@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
      			msg = xstrdup(msg);
    @@ transport.c: static void print_ref_status(char flag, const char *summary,
     +	if (to && to->remote_status) {
     +		val = parse_feature_value(to->remote_status, "ref", &len);
     +		if (val && len)
    -+		to_name = xmemdupz(val, len);
    ++			to_name = xmemdupz(val, len);
     +	}
     +
      	if (porcelain) {
    @@ transport.c: static int print_one_push_status(struct ref *ref, const char *dest,
     +					      porcelain, summary_width);
     +
     +	head = ref->remote_status;
    -+	begin = strstr(head, "ref=");
    -+	if (!begin)
    -+		begin = ref->remote_status;
    ++	begin = head;
     +	for (;;) {
     +		char *end;
     +		struct object_id old_oid;
-:  ---------- > 5:  c3ad1ab5a4 receive-pack: feed extended_status to post-receive
5:  a8860faa17 = 6:  d9226a4898 refs.c: refactor to reuse ref_is_hidden()
6:  46a839ddf6 ! 7:  7143e2c501 receive-pack: new config receive.procReceiveRefs
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +	if (proc_receive_refs.nr > 0) {
     +		struct strbuf refname_full = STRBUF_INIT;
     +		size_t prefix_len;
    -+
    -+		strbuf_addstr(&refname_full, get_git_namespace());
    -+		prefix_len = refname_full.len;
      
     -		/* TODO: replace the fixed prefix by looking up git config variables. */
     -		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
     -			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     -			run_proc_receive = 1;
    ++		strbuf_addstr(&refname_full, get_git_namespace());
    ++		prefix_len = refname_full.len;
    ++
     +		for (cmd = commands; cmd; cmd = cmd->next) {
     +			if (!should_process_cmd(cmd))
     +				continue;
    @@ builtin/receive-pack.c: int cmd_receive_pack(int argc, const char **argv, const
      	return 0;
      }
     
    - ## t/t5411/common-test-cases.sh ##
    -@@ t/t5411/common-test-cases.sh: test_expect_success "cleanup" '
    - 	)
    - '
    - 
    + ## t/t5411/test-0010-proc-receive-settings.sh (new) ##
    +@@
     +test_expect_success "add two receive.procReceiveRefs settings" '
     +	(
     +		cd "$upstream" &&
     +		git config --add receive.procReceiveRefs refs/for &&
     +		git config --add receive.procReceiveRefs refs/review/
     +	)
    ++'
    +
    + ## t/t5411/test-0040-process-all-refs.sh (new) ##
    +@@
    ++test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
    ++	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
    ++	git -C "$upstream" config --add receive.procReceiveRefs refs
     +'
     +
    - # Refs of upstream : master(A)
    - # Refs of workbench: master(A)  tags/v123
    - # git push         :                       next(A)  refs/for/master/topic(A)
    -@@ t/t5411/common-test-cases.sh: test_expect_success "setup proc-receive hook (with extended status)" '
    - 	write_script "$upstream/hooks/proc-receive" <<-EOF
    - 	printf >&2 "# proc-receive hook\n"
    - 	test-tool proc-receive -v \
    --		-r "ok refs/for/a/b/c/topic" \
    -+		-r "ok refs/review/a/b/c/topic" \
    - 		-r "alt refs/for/next/topic refs/pull/123/head" \
    - 		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
    - 	EOF
    -@@ t/t5411/common-test-cases.sh: test_expect_success "setup proc-receive hook (with extended status)" '
    - 
    - # Refs of upstream : master(A)
    - # Refs of workbench: master(A)  tags/v123
    --# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+# git push         :                       refs/for/next/topic(A)  refs/review/a/b/c/topic(A)  refs/for/master/topic(A)
    - test_expect_success "proc-receive: report with extended status" '
    - 	git -C workbench push origin \
    - 		HEAD:refs/for/next/topic \
    --		HEAD:refs/for/a/b/c/topic \
    -+		HEAD:refs/review/a/b/c/topic \
    - 		HEAD:refs/for/master/topic \
    - 		>out 2>&1 &&
    - 	make_user_friendly_and_stable_output <out >actual &&
    - 	cat >expect <<-EOF &&
    - 	remote: # pre-receive hook
    - 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    --	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    - 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    - 	remote: # proc-receive hook
    - 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    --	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    - 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    --	remote: proc-receive> ok refs/for/a/b/c/topic
    -+	remote: proc-receive> ok refs/review/a/b/c/topic
    - 	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    - 	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
    - 	remote: # post-receive hook
    - 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    --	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/a/b/c/topic
    - 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    - 	To <URL/of/upstream.git>
    - 	 * [new reference] HEAD -> refs/pull/123/head
    --	 * [new reference] HEAD -> refs/for/a/b/c/topic
    -+	 * [new reference] HEAD -> refs/review/a/b/c/topic
    - 	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
    - 	EOF
    - 	test_cmp expect actual &&
    -@@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update of mixed refs" '
    - 	EOF
    - 	test_cmp expect actual
    - '
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "setup upstream branches ($PROTOCOL)" '
    ++	(
    ++		cd "$upstream" &&
    ++		git update-ref refs/heads/master $B &&
    ++		git update-ref refs/heads/foo $A &&
    ++		git update-ref refs/heads/bar $A &&
    ++		git update-ref refs/heads/baz $A
    ++	)
     +
    -+test_expect_success "config add receive.procReceiveRefs = refs" '
    -+	git -C "$upstream" config --add receive.procReceiveRefs refs
     +'
     +
    -+test_expect_success "setup proc-receive hook" '
    ++test_expect_success "setup proc-receive hook ($PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update o
     +# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
    -+test_expect_success "proc-receive: process all refs" '
    ++test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
     +	git -C workbench push -f origin \
     +		HEAD:refs/heads/master \
     +		:refs/heads/foo \
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update o
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
     +	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
     +	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
    ++	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
     +	To <URL/of/upstream.git>
     +	 <OID-A>..<OID-B> <COMMIT-B> -> bar
     +	 - [deleted] foo
    @@ t/t5411/common-test-cases.sh: test_expect_success "proc-receive: report update o
     +	EOF
     +	test_cmp expect actual
     +'
    ++
    ++# Refs of upstream : master(A)             bar(A)  baz(B)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	(
    ++		cd "$upstream" &&
    ++		git update-ref -d refs/heads/bar &&
    ++		git update-ref -d refs/heads/baz
    ++	)
    ++'
    +
    + ## t/t5411/test-0041-process-all-refs--porcelain.sh (new) ##
    +@@
    ++test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
    ++	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
    ++	git -C "$upstream" config --add receive.procReceiveRefs refs
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "setup upstream branches ($PROTOCOL)" '
    ++	(
    ++		cd "$upstream" &&
    ++		git update-ref refs/heads/master $B &&
    ++		git update-ref refs/heads/foo $A &&
    ++		git update-ref refs/heads/bar $A &&
    ++		git update-ref refs/heads/baz $A
    ++	)
    ++
    ++'
    ++
    ++test_expect_success "setup proc-receive hook ($PROTOCOL)" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ft refs/heads/master" \
    ++		-r "ft refs/heads/foo" \
    ++		-r "ft refs/heads/bar" \
    ++		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$A new-oid=$B " \
    ++		-r "alt refs/for/next/topic refs/pull/124/head old-oid=$B new-oid=$A forced-update"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
    ++test_expect_success "proc-receive: process all refs (--porcelain) ($PROTOCOL)" '
    ++	git -C workbench push --porcelain -f origin \
    ++		HEAD:refs/heads/master \
    ++		:refs/heads/foo \
    ++		$B:refs/heads/bar \
    ++		HEAD:refs/for/master/topic \
    ++		HEAD:refs/for/next/topic \
    ++		>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	remote: # pre-receive hook
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
    ++	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
    ++	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: # proc-receive hook
    ++	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
    ++	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
    ++	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive> ft refs/heads/master
    ++	remote: proc-receive> ft refs/heads/foo
    ++	remote: proc-receive> ft refs/heads/bar
    ++	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> alt refs/for/next/topic refs/pull/124/head old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
    ++	remote: # post-receive hook
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
    ++	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
    ++	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
    ++	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
    ++	To <URL/of/upstream.git>
    ++	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
    ++	-    :refs/heads/foo    [deleted]
    ++	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
    ++	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
    ++	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
    ++	Done
    ++	EOF
    ++	test_cmp expect actual &&
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-B> refs/heads/bar
    ++	<COMMIT-A> refs/heads/baz
    ++	<COMMIT-A> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(A)             bar(A)  baz(B)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup ($PROTOCOL)" '
    ++	(
    ++		cd "$upstream" &&
    ++		git update-ref -d refs/heads/bar &&
    ++		git update-ref -d refs/heads/baz
    ++	)
    ++'
7:  a357234e97 = 8:  98702cf3e9 doc: add documentation for the proc-receive hook

---

Jiang Xin (8):
  transport: not report a non-head push as a branch
  connect: export parse_feature_value()
  receive-pack: add new proc-receive hook
  send-pack: extension for client-side status report
  receive-pack: feed extended_status to post-receive
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt              |  14 +
 Documentation/githooks.txt                    |  58 +++
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 442 +++++++++++++++++-
 connect.c                                     |   3 +-
 connect.h                                     |   1 +
 refs.c                                        |  13 +-
 refs.h                                        |   1 +
 remote.c                                      |  21 +
 remote.h                                      |   3 +
 send-pack.c                                   |  11 +-
 t/helper/test-proc-receive.c                  | 175 +++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411-proc-receive-hook.sh                  | 105 +++++
 t/t5411/common-functions.sh                   |  56 +++
 t/t5411/test-0000-standard-git-push.sh        |  84 ++++
 .../test-0001-standard-git-push--porcelain.sh |  87 ++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0011-no-hook-error.sh            |  64 +++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 +++
 t/t5411/test-0013-bad-protocol.sh             | 225 +++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 +++++++
 t/t5411/test-0020-report-ng.sh                |  67 +++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 +++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 ++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 ++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 ++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-alt.sh               | 229 +++++++++
 t/t5411/test-0033-report-alt--porcelain.sh    | 236 ++++++++++
 t/t5411/test-0034-report-ft.sh                |  42 ++
 t/t5411/test-0035-report-ft--porcelain.sh     |  43 ++
 .../test-0036-report-multi-alt-for-one-ref.sh | 167 +++++++
 ...report-multi-alt-for-one-ref--porcelain.sh | 171 +++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  74 +++
 .../test-0039-report-mixed-refs--porcelain.sh |  75 +++
 t/t5411/test-0040-process-all-refs.sh         |  93 ++++
 .../test-0041-process-all-refs--porcelain.sh  |  94 ++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport-helper.c                            |  64 +--
 transport.c                                   |  83 +++-
 48 files changed, 3506 insertions(+), 61 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-alt.sh
 create mode 100644 t/t5411/test-0033-report-alt--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-alt-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

-- 


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v13 1/8] transport: not report a non-head push as a branch
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 0/8] " Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 2/8] connect: export parse_feature_value() Jiang Xin
                                       ` (6 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh                  | 105 ++++++++++++++++++
 t/t5411/common-functions.sh                   |  56 ++++++++++
 t/t5411/test-0000-standard-git-push.sh        |  84 ++++++++++++++
 .../test-0001-standard-git-push--porcelain.sh |  87 +++++++++++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++++++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport.c                                   |   9 +-
 8 files changed, 406 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..33d4e019ae
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+setup_upstream_and_workbench () {
+	# Refs of upstream : master(A)
+	# Refs of workbench: master(A)  tags/v123
+	test_expect_success "setup upstream and workbench" '
+		rm -rf upstream.git &&
+		rm -rf workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git remote add origin ../upstream.git &&
+			git update-ref refs/heads/master $A &&
+			git tag -m "v123" v123 $A &&
+			git push origin $A:refs/heads/master
+		) &&
+		TAG=$(git -C workbench rev-parse v123) &&
+
+		# setup pre-receive hook
+		write_script upstream.git/hooks/pre-receive <<-\EOF &&
+		exec >&2
+		echo "# pre-receive hook"
+		while read old new ref
+		do
+			echo "pre-receive< $old $new $ref"
+		done
+		EOF
+
+		# setup post-receive hook
+		write_script upstream.git/hooks/post-receive <<-\EOF &&
+		exec >&2
+		echo "# post-receive hook"
+		while read old new ref
+		do
+			echo "post-receive< $old $new $ref"
+		done
+		EOF
+
+		upstream=upstream.git
+	'
+}
+
+run_proc_receive_hook_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	file | local)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Include test cases for both file and HTTP protocol
+	for t in  "$TEST_DIRECTORY"/t5411/test-*.sh
+	do
+		. "$t"
+	done
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run test cases for 'proc-receive' hook on local file protocol.
+run_proc_receive_hook_test local
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup for HTTP protocol" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Run test cases for 'proc-receive' hook on HTTP protocol.
+run_proc_receive_hook_test http
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6580bebd8e
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,56 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
new file mode 100644
index 0000000000..2ee35d17b1
--- /dev/null
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -0,0 +1,84 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "non-force git-push ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> next
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "force git-push ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
new file mode 100644
index 0000000000..6b7ad15543
--- /dev/null
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -0,0 +1,87 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "non-force git-push (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/next    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "force git-push (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	-    :refs/heads/next    [deleted]
+	*    refs/tags/v123:refs/tags/v123    [new tag]
+	*    HEAD:refs/review/master/topic    [new reference]
+	*    HEAD:refs/heads/a/b/c    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
+
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
new file mode 100644
index 0000000000..71203fbfd5
--- /dev/null
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -0,0 +1,33 @@
+test_expect_success "setup pre-receive hook (declined, $PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (pre-receive hook declined)
+	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
new file mode 100644
index 0000000000..0bf4e44657
--- /dev/null
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup pre-receive hook (declined, $PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined (--porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (pre-receive hook declined)
+	!    HEAD:refs/heads/next    [remote rejected] (pre-receive hook declined)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9c6218f568..77938db77f 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index a07e39564c..73defc4a5a 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v13 2/8] connect: export parse_feature_value()
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 0/8] " Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 1/8] transport: not report a non-head push as a branch Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 3/8] receive-pack: add new proc-receive hook Jiang Xin
                                       ` (5 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Export `parse_feature_value()` to parse extended status of report for
"proc-receive" and "receive-pack".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 connect.c | 3 +--
 connect.h | 1 +
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/connect.c b/connect.c
index b6451ab5e8..08e0616726 100644
--- a/connect.c
+++ b/connect.c
@@ -18,7 +18,6 @@
 
 static char *server_capabilities_v1;
 static struct argv_array server_capabilities_v2 = ARGV_ARRAY_INIT;
-static const char *parse_feature_value(const char *, const char *, int *);
 
 static int check_ref(const char *name, unsigned int flags)
 {
@@ -447,7 +446,7 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
 	return list;
 }
 
-static const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
+const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
 {
 	int len;
 
diff --git a/connect.h b/connect.h
index 5f2382e018..17b3252cbc 100644
--- a/connect.h
+++ b/connect.h
@@ -12,6 +12,7 @@ int finish_connect(struct child_process *conn);
 int git_connection_is_socket(struct child_process *conn);
 int server_supports(const char *feature);
 int parse_feature_request(const char *features, const char *feature);
+const char *parse_feature_value(const char *feature_list, const char *feature, int *len_ret);
 const char *server_feature_value(const char *feature, int *len_ret);
 int url_is_local_not_ssh(const char *url);
 
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v13 3/8] receive-pack: add new proc-receive hook
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                       ` (2 preceding siblings ...)
  2020-04-18 16:03                     ` [PATCH v13 2/8] connect: export parse_feature_value() Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 4/8] send-pack: extension for client-side status report Jiang Xin
                                       ` (4 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ft <ref>)
    # OK, but has an alternate reference.  The alternate reference name
    # is given in the third parameter, and other status can be given in
    # key-value pairs.
    H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
                forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in key-value pairs as extended status of the report
line.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" is extended
using a backward compatible way by adding key-value pairs after an null
character, like:

    # OK, run this command successfully with optional extended-status.
    ok <reference>\0ref=refs/pull/123/head old-oid=...

    # NO, I reject it.
    ng <reference> <error message>

Parsing the extended status of the report lines for "send-pack" is not
implemented in this commit, so we can make a compatible test on old
version of a Git client.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 356 +++++++++++++++++-
 t/helper/test-proc-receive.c                  | 175 +++++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411/test-0011-no-hook-error.sh            |  64 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 ++++
 t/t5411/test-0013-bad-protocol.sh             | 225 +++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++++
 t/t5411/test-0020-report-ng.sh                |  67 ++++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 ++++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 +++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 +++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 ++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-alt.sh               | 229 +++++++++++
 t/t5411/test-0033-report-alt--porcelain.sh    | 236 ++++++++++++
 t/t5411/test-0034-report-ft.sh                |  42 +++
 t/t5411/test-0035-report-ft--porcelain.sh     |  43 +++
 .../test-0036-report-multi-alt-for-one-ref.sh | 155 ++++++++
 ...report-multi-alt-for-one-ref--porcelain.sh | 159 ++++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  74 ++++
 .../test-0039-report-mixed-refs--porcelain.sh |  75 ++++
 27 files changed, 2581 insertions(+), 9 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-alt.sh
 create mode 100644 t/t5411/test-0033-report-alt--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-alt-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh

diff --git a/Makefile b/Makefile
index ef1ff2228f..1c52c280ce 100644
--- a/Makefile
+++ b/Makefile
@@ -725,6 +725,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2cc18bbffd..0081bf1415 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,15 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
+	const char *extended_status;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -817,6 +821,307 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	struct strbuf extended_status = STRBUF_INIT;
+	int code = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *status;
+		char *refname;
+		char *remains = NULL;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+		status = reader->line;
+		refname = strchr(status, ' ');
+		if (refname) {
+			*refname++ = '\0';
+			remains = strchr(refname, ' ');
+			if (remains)
+				*remains++ = '\0';
+		}
+		if (!refname || strlen(refname) == 0) {
+			strbuf_addf(errmsg, "proc-receive expected '<status> <ref> ...', got '%s'\n",
+				    reader->line);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			continue;
+		}
+		if (!strcmp(status, "ng")) {
+			if (remains)
+				hint->error_string = xstrdup(remains);
+			else
+				hint->error_string = "failed";
+			code = -1;
+		} else if (!strcmp("ft", status)) {
+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
+			hint->run_proc_receive = 0;
+		} else if (!strcmp("alt", status)) {
+			char *alt_ref = NULL;
+			char *p1, *p2;
+
+			if (!remains) {
+				strbuf_addf(errmsg, "proc-receive expected 'alt <ref> <alt-ref> ...', got 'alt %s'\n",
+					    refname);
+				continue;
+			}
+			p1 = strchr(remains, '=');
+			p2 = strchr(remains, ' ');
+			if (!p1) {
+				/*
+				 * "alt <ref> <alt-ref>", or
+				 * "alt <ref> <alt-ref> forced-update"
+				 */
+				alt_ref = remains;
+				remains = p2;
+				if (remains)
+					*remains++ = '\0';
+			} else if (!p2) {
+				/* "alt <ref> old-oid=<oid>" */
+				alt_ref = NULL;
+			} else if (p1 > p2) {
+				/* "alt <ref> <alt-ref> old-oid=<oid>" */
+				alt_ref = remains;
+				remains = p2;
+				if (remains)
+					*remains++ = '\0';
+			} else {
+				/* "alt <ref> old-oid=<oid1> new-oid=<oid2>" */
+				alt_ref = NULL;
+			}
+
+			if (hint->extended_status)
+				strbuf_addstr(&extended_status, hint->extended_status);
+
+			/*
+			 * Already received an "ok <ref>" response, add an
+			 * additional "ref=<ref>" field.
+			 */
+			if (extended_status.len == 0 &&
+			    hint->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)
+				strbuf_addf(&extended_status, "ref=%s", refname);
+
+			if (alt_ref)
+				strbuf_addf(&extended_status, "%sref=%s",
+					    extended_status.len > 0 ? " ": "",
+					    alt_ref);
+			else if (extended_status.len > 0)
+				/*
+				 * Start new extended status for refname, if it
+				 * is not the first report for this reference.
+				 */
+				strbuf_addf(&extended_status, " ref=%s", refname);
+
+			if (remains) {
+				const char *val;
+				int len;
+
+				val = parse_feature_value(remains, "old-oid", &len);
+				if (val && len > 0)
+					if (!parse_oid_hex(val, &old_oid, &val))
+						strbuf_addf(&extended_status, "%sold-oid=%s",
+							   extended_status.len > 0 ? " ": "",
+							   oid_to_hex(&old_oid));
+
+				val = parse_feature_value(remains, "new-oid", &len);
+				if (val && len > 0)
+					if (!parse_oid_hex(val, &new_oid, &val))
+						strbuf_addf(&extended_status, "%snew-oid=%s",
+							   extended_status.len > 0 ? " ": "",
+							   oid_to_hex(&new_oid));
+
+				if (parse_feature_request(remains, "forced-update"))
+					strbuf_addf(&extended_status, "%sforced-update",
+						    extended_status.len > 0 ? " ": "");
+			}
+
+			if (extended_status.len > 0) {
+				free((void *)hint->extended_status);
+				hint->extended_status = strbuf_detach(&extended_status, NULL);
+			}
+		} else if (strcmp("ok", status)) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    status, refname);
+			code = -1;
+			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
+			continue;
+		}
+
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->error_string &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "proc-receive failed to report status";
+		    code = -1;
+		}
+
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int pr_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					pr_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (pr_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1392,7 +1697,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1432,7 +1737,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1468,6 +1773,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1497,6 +1803,21 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1523,6 +1844,12 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
+				cmd->error_string = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -1899,12 +2226,23 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
-			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
-		else
-			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+		if (!cmd->error_string) {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ok %s%c%s\n",
+						 cmd->ref_name, '\0',
+						 cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+		} else {
+			if (cmd->extended_status)
+				packet_buf_write(&buf, "ng %s %s%c%s\n",
+						 cmd->ref_name, cmd->error_string,
+						 '\0', cmd->extended_status);
+			else
+				packet_buf_write(&buf, "ng %s %s\n",
+						 cmd->ref_name, cmd->error_string);
+		}
 	}
 	packet_buf_flush(&buf);
 
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..6ace1863b0
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,175 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int die_version = 0;
+static int die_readline = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static int verbose = 0;
+static int version = 1;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1 || die_version)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ' ||
+		    die_readline)
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_BOOL(0, "die-version", &die_version,
+			 "die during version negotiation"),
+		OPT_BOOL(0, "die-readline", &die_readline,
+			 "die when readline"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 31eedcd241..f865433a61 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -44,6 +44,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 4eb5e6609e..059008cf22 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -34,6 +34,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
new file mode 100644
index 0000000000..bb6ec92a92
--- /dev/null
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -0,0 +1,64 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
new file mode 100644
index 0000000000..e3bd0962ab
--- /dev/null
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -0,0 +1,66 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref (--porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push (--porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
new file mode 100644
index 0000000000..a375ddffd9
--- /dev/null
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -0,0 +1,225 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: bad protocol version: 1
+	remote: error: proc-receive version "0" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: protocol error: expected "old new ref", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive expected "<status> <ref> ...", got "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
new file mode 100644
index 0000000000..96bbed08fe
--- /dev/null
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -0,0 +1,160 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, --porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, --porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, --porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive expected "<status> <ref> ...", got "ok"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, --porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
new file mode 100644
index 0000000000..f726b7ca9c
--- /dev/null
+++ b/t/t5411/test-0020-report-ng.sh
@@ -0,0 +1,67 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
new file mode 100644
index 0000000000..5c90d63287
--- /dev/null
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -0,0 +1,69 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, --porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, --porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
new file mode 100644
index 0000000000..92a415b929
--- /dev/null
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
new file mode 100644
index 0000000000..f10715c2dc
--- /dev/null
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -0,0 +1,46 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref (--porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
new file mode 100644
index 0000000000..c3946f329a
--- /dev/null
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
new file mode 100644
index 0000000000..b5a5d9b992
--- /dev/null
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference (--porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
new file mode 100644
index 0000000000..d0c4da8b23
--- /dev/null
+++ b/t/t5411/test-0026-push-options.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
new file mode 100644
index 0000000000..243580db93
--- /dev/null
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -0,0 +1,82 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options (--porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		--porcelain \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options (--porcelain, $PROTOCOL)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
new file mode 100644
index 0000000000..44c99d3831
--- /dev/null
+++ b/t/t5411/test-0030-report-ok.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
new file mode 100644
index 0000000000..2a10bdc749
--- /dev/null
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -0,0 +1,36 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-alt.sh b/t/t5411/test-0032-report-alt.sh
new file mode 100644
index 0000000000..dec4afc4c1
--- /dev/null
+++ b/t/t5411/test-0032-report-alt.sh
@@ -0,0 +1,229 @@
+test_expect_success "setup proc-receive hook (alt <ref>, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref>, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic
+	remote: error: proc-receive expected "alt <ref> <alt-ref> ...", got "alt refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref>, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, $PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/pull/123/head forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update, $PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> old-oid=X, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, $PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic old-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, $PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X new-oid=Y, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, $PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (with multiple alt reports, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "alt refs/for/next/topic refs/pull/123/head" \
+		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: with multiple alt reports ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
+	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0033-report-alt--porcelain.sh b/t/t5411/test-0033-report-alt--porcelain.sh
new file mode 100644
index 0000000000..49d76892ab
--- /dev/null
+++ b/t/t5411/test-0033-report-alt--porcelain.sh
@@ -0,0 +1,236 @@
+test_expect_success "setup proc-receive hook (alt <ref>, --porcelain, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref>, --porcelain, $PROTOCOL)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic
+	remote: error: proc-receive expected "alt <ref> <alt-ref> ...", got "alt refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref>, --porcelain, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, --porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> forced-update, --porcelain, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/pull/123/head forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update, --porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> old-oid=X, --porcelain, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, --porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X, --porcelain, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic old-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, --porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X new-oid=Y, --porcelain, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, --porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (with multiple alt reports, --porcelain, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "alt refs/for/next/topic refs/pull/123/head" \
+		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: with multiple alt reports (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
+	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/for/a/b/c/topic    [new reference]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
new file mode 100644
index 0000000000..a25fb0505e
--- /dev/null
+++ b/t/t5411/test-0034-report-ft.sh
@@ -0,0 +1,42 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ft refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ft refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
new file mode 100644
index 0000000000..e5c6eae544
--- /dev/null
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -0,0 +1,43 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ft refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ft refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    <COMMIT-B>:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0036-report-multi-alt-for-one-ref.sh b/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
new file mode 100644
index 0000000000..fc019f709b
--- /dev/null
+++ b/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
@@ -0,0 +1,155 @@
+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 1st resp, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st resp ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 2nd resp, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$B new-oid=$A forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd resp ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ok and alt for the same ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "alt refs/for/master/topic refs/changes/24/124/1 new-oid=$B old-oid=$A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report ok and alt for the same ref ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple responses, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/changes/23/123/1" \
+		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report multiple response ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
new file mode 100644
index 0000000000..22607b135e
--- /dev/null
+++ b/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
@@ -0,0 +1,159 @@
+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 1st resp, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st resp (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 2nd resp, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$B new-oid=$A forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd resp (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ok and alt for the same ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "alt refs/for/master/topic refs/changes/24/124/1 new-oid=$B old-oid=$A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report ok and alt for the same ref (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple responses, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "alt refs/for/master/topic refs/changes/23/123/1" \
+		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: report multiple response (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
new file mode 100644
index 0000000000..dd03bf1538
--- /dev/null
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -0,0 +1,74 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic" \
+		-r "alt refs/for/master/topic refs/for/master/topic old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> alt refs/for/master/topic refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
new file mode 100644
index 0000000000..e237f40db7
--- /dev/null
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -0,0 +1,75 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic" \
+		-r "alt refs/for/master/topic refs/for/master/topic old-oid=$A new-oid=$B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs (--porcelain, $PROTOCOL)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> alt refs/for/master/topic refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/bar    [new branch]
+	*    HEAD:refs/heads/baz    [new branch]
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/heads/foo    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v13 4/8] send-pack: extension for client-side status report
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                       ` (3 preceding siblings ...)
  2020-04-18 16:03                     ` [PATCH v13 3/8] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 5/8] receive-pack: feed extended_status to post-receive Jiang Xin
                                       ` (3 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This is the client-side extension for status report of git-push.

The "proc-receive" hook may receive a command for a pseudo-reference
with a zero-old as its old-oid, while the result of the hook may point
to an alternate reference and the reference may exist already with a
non-zero old-oid.  Git client may receive a report with extened status
as the following example:

    ok refs/for/master/topic\0ref=refs/pull/123/head old-oid=...

Parse the key-value pairs in the extended status after the null
character of the report, and show the proper reference name, old-oid
and new-oid to the user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 remote.c                                      | 21 ++++++
 remote.h                                      |  3 +
 send-pack.c                                   | 11 ++-
 t/t5411/test-0032-report-alt.sh               | 14 ++--
 t/t5411/test-0033-report-alt--porcelain.sh    | 14 ++--
 .../test-0036-report-multi-alt-for-one-ref.sh | 12 ++-
 ...report-multi-alt-for-one-ref--porcelain.sh | 12 ++-
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 transport-helper.c                            | 64 ++++++++--------
 transport.c                                   | 74 +++++++++++++++++--
 11 files changed, 169 insertions(+), 60 deletions(-)

diff --git a/remote.c b/remote.c
index c43196ec06..4174746bb9 100644
--- a/remote.c
+++ b/remote.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "config.h"
+#include "connect.h"
 #include "remote.h"
 #include "refs.h"
 #include "refspec.h"
@@ -2311,3 +2312,23 @@ void apply_push_cas(struct push_cas_option *cas,
 	for (ref = remote_refs; ref; ref = ref->next)
 		apply_cas(cas, remote, ref);
 }
+
+void update_ref_from_remote_status(struct ref *ref)
+{
+	char *val;
+	int len;
+
+	if (!ref->remote_status)
+		return;
+
+	val = (char *)parse_feature_value(ref->remote_status, "old-oid", &len);
+	if (val && len)
+		get_oid_hex(val, &ref->old_oid);
+
+	val = (char *)parse_feature_value(ref->remote_status, "new-oid", &len);
+	if (val && len)
+		get_oid_hex(val, &ref->new_oid);
+
+	if (parse_feature_request(ref->remote_status, "forced-update"))
+		ref->forced_update = 1;
+}
diff --git a/remote.h b/remote.h
index 11d8719b58..09cf7014e3 100644
--- a/remote.h
+++ b/remote.h
@@ -345,4 +345,7 @@ int parseopt_push_cas_option(const struct option *, const char *arg, int unset);
 int is_empty_cas(const struct push_cas_option *);
 void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
 
+/* Parse key-value pairs of remote_status and update the reference accordingly */
+void update_ref_from_remote_status(struct ref *ref);
+
 #endif
diff --git a/send-pack.c b/send-pack.c
index a7c53193c9..6e2c7a0c0c 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -159,6 +159,9 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 	while (1) {
 		const char *refname;
 		char *msg;
+		char *extended_status = NULL;
+		int len;
+
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
@@ -167,10 +170,13 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			break;
 		}
 
+		len = strlen(reader->line);
 		refname = reader->line + 3;
 		msg = strchr(refname, ' ');
 		if (msg)
 			*msg++ = '\0';
+		if (reader->pktlen > len)
+			extended_status = (char *)reader->line + len + 1;
 
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
@@ -192,7 +198,10 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			hint->status = REF_STATUS_OK;
 		else
 			hint->status = REF_STATUS_REMOTE_REJECT;
-		hint->remote_status = xstrdup_or_null(msg);
+		if (msg)
+			hint->remote_status = xstrdup(msg);
+		else if (extended_status)
+			hint->remote_status = xstrdup_or_null(extended_status);
 		/* start our next search from the next ref */
 		hint = hint->next;
 	}
diff --git a/t/t5411/test-0032-report-alt.sh b/t/t5411/test-0032-report-alt.sh
index dec4afc4c1..bf5faf4f2e 100644
--- a/t/t5411/test-0032-report-alt.sh
+++ b/t/t5411/test-0032-report-alt.sh
@@ -52,7 +52,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, $PROTOCOL)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -82,7 +82,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -112,7 +112,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, $P
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -142,7 +142,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, $PROTOCOL)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -172,7 +172,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, $P
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -214,9 +214,9 @@ test_expect_success "proc-receive: with multiple alt reports ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0033-report-alt--porcelain.sh b/t/t5411/test-0033-report-alt--porcelain.sh
index 49d76892ab..3aea911407 100644
--- a/t/t5411/test-0033-report-alt--porcelain.sh
+++ b/t/t5411/test-0033-report-alt--porcelain.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, --porcelain,
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -84,7 +84,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -115,7 +115,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, --
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -146,7 +146,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, --porcelain,
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -177,7 +177,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, --
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual
@@ -220,9 +220,9 @@ test_expect_success "proc-receive: with multiple alt reports (--porcelain, $PROT
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
-	*    HEAD:refs/for/master/topic    [new reference]
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0036-report-multi-alt-for-one-ref.sh b/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
index fc019f709b..d82b79ffff 100644
--- a/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
@@ -27,7 +27,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st r
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -67,7 +69,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd r
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -106,6 +110,7 @@ test_expect_success "proc-receive: report ok and alt for the same ref ($PROTOCOL
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -143,7 +148,8 @@ test_expect_success "proc-receive: report multiple response ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
index 22607b135e..8cca63c2de 100644
--- a/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
@@ -27,7 +27,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st r
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -68,7 +70,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd r
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -108,6 +112,7 @@ test_expect_success "proc-receive: report ok and alt for the same ref (--porcela
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/changes/24/124/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -146,7 +151,8 @@ test_expect_success "proc-receive: report multiple response (--porcelain, $PROTO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/23/123/1    [new reference]
+	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index dd03bf1538..566cb6e098 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -46,7 +46,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index e237f40db7..4d617abb1a 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -46,7 +46,7 @@ test_expect_success "proc-receive: report update of mixed refs (--porcelain, $PR
 	*    HEAD:refs/heads/baz    [new branch]
 	*    HEAD:refs/for/next/topic    [new reference]
 	*    HEAD:refs/heads/foo    [new branch]
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/transport-helper.c b/transport-helper.c
index a46afcb69d..0c835c5495 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -747,37 +747,39 @@ static int push_update_ref_status(struct strbuf *buf,
 			msg = xstrdup(msg);
 		strbuf_release(&msg_buf);
 
-		if (!strcmp(msg, "no match")) {
-			status = REF_STATUS_NONE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "up to date")) {
-			status = REF_STATUS_UPTODATE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "non-fast forward")) {
-			status = REF_STATUS_REJECT_NONFASTFORWARD;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "already exists")) {
-			status = REF_STATUS_REJECT_ALREADY_EXISTS;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "fetch first")) {
-			status = REF_STATUS_REJECT_FETCH_FIRST;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "needs force")) {
-			status = REF_STATUS_REJECT_NEEDS_FORCE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "stale info")) {
-			status = REF_STATUS_REJECT_STALE;
-			FREE_AND_NULL(msg);
-		}
-		else if (!strcmp(msg, "forced update")) {
-			forced = 1;
-			FREE_AND_NULL(msg);
+		if (status != REF_STATUS_OK) {
+			if (!strcmp(msg, "no match")) {
+				status = REF_STATUS_NONE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "up to date")) {
+				status = REF_STATUS_UPTODATE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "non-fast forward")) {
+				status = REF_STATUS_REJECT_NONFASTFORWARD;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "already exists")) {
+				status = REF_STATUS_REJECT_ALREADY_EXISTS;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "fetch first")) {
+				status = REF_STATUS_REJECT_FETCH_FIRST;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "needs force")) {
+				status = REF_STATUS_REJECT_NEEDS_FORCE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "stale info")) {
+				status = REF_STATUS_REJECT_STALE;
+				FREE_AND_NULL(msg);
+			}
+			else if (!strcmp(msg, "forced update")) {
+				forced = 1;
+				FREE_AND_NULL(msg);
+			}
 		}
 	}
 
diff --git a/transport.c b/transport.c
index 73defc4a5a..ecdcaae90d 100644
--- a/transport.c
+++ b/transport.c
@@ -463,11 +463,23 @@ static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     int porcelain, int summary_width)
 {
+	char *to_name = NULL;
+	const char *val;
+	int len;
+
+	if (to && to->remote_status) {
+		val = parse_feature_value(to->remote_status, "ref", &len);
+		if (val && len)
+			to_name = xmemdupz(val, len);
+	}
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name,
+				to_name ? to_name : to->name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag,
+				to_name ? to_name : to->name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +493,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name ? to_name : to->name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name ? to_name : to->name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -491,6 +505,7 @@ static void print_ref_status(char flag, const char *summary,
 		}
 		fputc('\n', stderr);
 	}
+	free(to_name);
 }
 
 static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
@@ -531,8 +546,8 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
-				 int porcelain, int summary_width)
+static int _print_one_push_status(struct ref *ref, const char *dest, int count,
+				  int porcelain, int summary_width)
 {
 	if (!count) {
 		char *url = transport_anonymize_url(dest);
@@ -602,6 +617,53 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	char *head;
+	char *begin;
+	int n = 0;
+
+	if (!ref->remote_status)
+		return _print_one_push_status(ref, dest, count,
+					      porcelain, summary_width);
+
+	head = ref->remote_status;
+	begin = head;
+	for (;;) {
+		char *end;
+		struct object_id old_oid;
+		struct object_id new_oid;
+		int forced_update;
+
+		end = strstr(begin + 4, "ref=");
+		if (end)
+			*(end-1) = '\0';
+
+		oidcpy(&old_oid, &ref->old_oid);
+		oidcpy(&new_oid, &ref->new_oid);
+		forced_update = ref->forced_update;
+
+		ref->remote_status = begin;
+		update_ref_from_remote_status(ref);
+		_print_one_push_status(ref, dest, count + n++,
+				       porcelain, summary_width);
+
+		oidcpy(&ref->old_oid, &old_oid);
+		oidcpy(&ref->new_oid, &new_oid);
+		ref->forced_update = forced_update;
+
+		if (end) {
+			begin = end;
+			*(end-1) = ' ';
+		} else {
+			break;
+		}
+	}
+	ref->remote_status = head;
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v13 5/8] receive-pack: feed extended_status to post-receive
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                       ` (4 preceding siblings ...)
  2020-04-18 16:03                     ` [PATCH v13 4/8] send-pack: extension for client-side status report Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 6/8] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                                       ` (2 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When commands are fed to the "post-receive" hook, `extended_status` will
be parsed and the real old-oid, new-oid, reference name will feed to
the "post-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        | 57 +++++++++++++++++--
 t/t5411/test-0032-report-alt.sh               | 14 ++---
 t/t5411/test-0033-report-alt--porcelain.sh    | 14 ++---
 .../test-0036-report-multi-alt-for-one-ref.sh | 12 +++-
 ...report-multi-alt-for-one-ref--porcelain.sh | 12 +++-
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 7 files changed, 87 insertions(+), 26 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 0081bf1415..529ac01e1c 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -754,17 +754,66 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 {
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
+	static char *extended_status = NULL;
 
 	while (cmd &&
 	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
+	if (!bufp)
+		return 0; /* OK, can feed something. */
 	strbuf_reset(&state->buf);
-	strbuf_addf(&state->buf, "%s %s %s\n",
-		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
-		    cmd->ref_name);
-	state->cmd = cmd->next;
+	if (cmd->extended_status && extended_status == NULL)
+		extended_status = (char *)cmd->extended_status;
+	if (extended_status) {
+		struct object_id old_oid;
+		struct object_id new_oid;
+		char *end;
+		char *val;
+		int len;
+
+		end = strstr(extended_status + 4, "ref=");
+		if (end)
+			*(end-1) = '\0';
+
+		val = (char *)parse_feature_value(extended_status, "old-oid", &len);
+		if (val && len && !get_oid_hex(val, &old_oid))
+			strbuf_addf(&state->buf, "%s ",
+				    oid_to_hex(&old_oid));
+		else
+			strbuf_addf(&state->buf, "%s ",
+				    oid_to_hex(&cmd->old_oid));
+
+		val = (char *)parse_feature_value(extended_status, "new-oid", &len);
+		if (val && len && !get_oid_hex(val, &new_oid))
+			strbuf_addf(&state->buf, "%s ",
+				    oid_to_hex(&new_oid));
+		else
+			strbuf_addf(&state->buf, "%s ",
+				    oid_to_hex(&cmd->new_oid));
+
+		val = (char *)parse_feature_value(extended_status, "ref", &len);
+		if (val && len)
+			strbuf_add(&state->buf, val, len);
+		else
+			strbuf_addstr(&state->buf, cmd->ref_name);
+
+		strbuf_addch(&state->buf, '\n');
+
+		if (end) {
+			extended_status = end;
+			*(end-1) = ' ';
+		} else {
+			extended_status = NULL;
+			state->cmd = cmd->next;
+		}
+	} else {
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
+			    cmd->ref_name);
+		state->cmd = cmd->next;
+	}
 	if (bufp) {
 		*bufp = state->buf.buf;
 		*sizep = state->buf.len;
diff --git a/t/t5411/test-0032-report-alt.sh b/t/t5411/test-0032-report-alt.sh
index bf5faf4f2e..25bffd1127 100644
--- a/t/t5411/test-0032-report-alt.sh
+++ b/t/t5411/test-0032-report-alt.sh
@@ -50,7 +50,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, $PROTOCOL)"
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -80,7 +80,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -110,7 +110,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, $P
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
@@ -140,7 +140,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, $PROTOCOL)"
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
@@ -170,7 +170,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, $P
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
@@ -210,9 +210,9 @@ test_expect_success "proc-receive: with multiple alt reports ($PROTOCOL)" '
 	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
 	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
diff --git a/t/t5411/test-0033-report-alt--porcelain.sh b/t/t5411/test-0033-report-alt--porcelain.sh
index 3aea911407..9f1b577424 100644
--- a/t/t5411/test-0033-report-alt--porcelain.sh
+++ b/t/t5411/test-0033-report-alt--porcelain.sh
@@ -51,7 +51,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, --porcelain,
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -82,7 +82,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -113,7 +113,7 @@ test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, --
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
@@ -144,7 +144,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, --porcelain,
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
@@ -175,7 +175,7 @@ test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, --
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
@@ -216,9 +216,9 @@ test_expect_success "proc-receive: with multiple alt reports (--porcelain, $PROT
 	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
 	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
diff --git a/t/t5411/test-0036-report-multi-alt-for-one-ref.sh b/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
index d82b79ffff..1f6a4e10b2 100644
--- a/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-alt-for-one-ref.sh
@@ -25,7 +25,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st r
 	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
 	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 * [new reference] HEAD -> refs/changes/24/124/1
@@ -67,7 +69,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd r
 	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/24/124/1
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
@@ -108,6 +112,7 @@ test_expect_success "proc-receive: report ok and alt for the same ref ($PROTOCOL
 	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/1
@@ -146,7 +151,8 @@ test_expect_success "proc-receive: report multiple response ($PROTOCOL)" '
 	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
 	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/23/123/1
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
diff --git a/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
index 8cca63c2de..3a08a21b87 100644
--- a/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh
@@ -25,7 +25,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st r
 	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
 	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	*    HEAD:refs/changes/24/124/1    [new reference]
@@ -68,7 +70,9 @@ test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd r
 	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/24/124/1    [new reference]
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
@@ -110,6 +114,7 @@ test_expect_success "proc-receive: report ok and alt for the same ref (--porcela
 	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	     HEAD:refs/changes/24/124/1    <OID-A>..<OID-B>
@@ -149,7 +154,8 @@ test_expect_success "proc-receive: report multiple response (--porcelain, $PROTO
 	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
 	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/23/123/1    [new reference]
 	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 566cb6e098..5ac2771914 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -39,7 +39,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 4d617abb1a..55b408aca1 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -39,7 +39,7 @@ test_expect_success "proc-receive: report update of mixed refs (--porcelain, $PR
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
 	*    HEAD:refs/heads/bar    [new branch]
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v13 6/8] refs.c: refactor to reuse ref_is_hidden()
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                       ` (5 preceding siblings ...)
  2020-04-18 16:03                     ` [PATCH v13 5/8] receive-pack: feed extended_status to post-receive Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 7/8] receive-pack: new config receive.procReceiveRefs Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 8/8] doc: add documentation for the proc-receive hook Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 13 +++++++++----
 refs.h |  1 +
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 1ab0bb54d3..a20f56a463 100644
--- a/refs.c
+++ b/refs.c
@@ -1388,14 +1388,14 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 	return 0;
 }
 
-int ref_is_hidden(const char *refname, const char *refname_full)
+int ref_matches(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
@@ -1421,6 +1421,11 @@ int ref_is_hidden(const char *refname, const char *refname_full)
 	return 0;
 }
 
+int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_matches(hide_refs, refname, refname_full);
+}
+
 const char *find_descendant_ref(const char *dirname,
 				const struct string_list *extras,
 				const struct string_list *skip)
diff --git a/refs.h b/refs.h
index 545029c6d8..d02c72d3ad 100644
--- a/refs.h
+++ b/refs.h
@@ -739,6 +739,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_matches(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v13 7/8] receive-pack: new config receive.procReceiveRefs
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                       ` (6 preceding siblings ...)
  2020-04-18 16:03                     ` [PATCH v13 6/8] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  2020-04-18 16:03                     ` [PATCH v13 8/8] doc: add documentation for the proc-receive hook Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings match the reference names of the commands
which are sent from git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt              | 14 +++
 builtin/receive-pack.c                        | 43 +++++++--
 t/t5411/test-0010-proc-receive-settings.sh    |  7 ++
 t/t5411/test-0040-process-all-refs.sh         | 93 ++++++++++++++++++
 .../test-0041-process-all-refs--porcelain.sh  | 94 +++++++++++++++++++
 5 files changed, 244 insertions(+), 7 deletions(-)
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..e7b967feeb 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running the hook "proc-receive".
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 529ac01e1c..25ff718cdf 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -76,6 +76,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -228,6 +229,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_append(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1856,15 +1871,26 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
+
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2329,6 +2355,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2444,5 +2472,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/test-0010-proc-receive-settings.sh b/t/t5411/test-0010-proc-receive-settings.sh
new file mode 100644
index 0000000000..a36809927b
--- /dev/null
+++ b/t/t5411/test-0010-proc-receive-settings.sh
@@ -0,0 +1,7 @@
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
new file mode 100644
index 0000000000..bfb14c53fc
--- /dev/null
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -0,0 +1,93 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ft refs/heads/master" \
+		-r "ft refs/heads/foo" \
+		-r "ft refs/heads/bar" \
+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$A new-oid=$B " \
+		-r "alt refs/for/next/topic refs/pull/124/head old-oid=$B new-oid=$A forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ft refs/heads/master
+	remote: proc-receive> ft refs/heads/foo
+	remote: proc-receive> ft refs/heads/bar
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: proc-receive> alt refs/for/next/topic refs/pull/124/head old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
new file mode 100644
index 0000000000..02b24f4b16
--- /dev/null
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -0,0 +1,94 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ft refs/heads/master" \
+		-r "ft refs/heads/foo" \
+		-r "ft refs/heads/bar" \
+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$A new-oid=$B " \
+		-r "alt refs/for/next/topic refs/pull/124/head old-oid=$B new-oid=$A forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs (--porcelain) ($PROTOCOL)" '
+	git -C workbench push --porcelain -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ft refs/heads/master
+	remote: proc-receive> ft refs/heads/foo
+	remote: proc-receive> ft refs/heads/bar
+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-A> new-oid=<COMMIT-B>
+	remote: proc-receive> alt refs/for/next/topic refs/pull/124/head old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+	-    :refs/heads/foo    [deleted]
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v13 8/8] doc: add documentation for the proc-receive hook
  2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                       ` (7 preceding siblings ...)
  2020-04-18 16:03                     ` [PATCH v13 7/8] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-04-18 16:03                     ` Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-18 16:03 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 58 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 3dccab5375..a0ea829435 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,64 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ft <ref>)
+    # OK, but has an alternate reference.  The alternate reference name
+    # is given in the third parameter, and other status can be given in
+    # key-value pairs.
+    H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
+                forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook may return
+different OID and different reference name as extended status of the
+report line.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.24.1.15.g448c31058d.agit.4.5


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-15 18:34                         ` Junio C Hamano
@ 2020-04-27 17:00                           ` Jiang Xin
  2020-04-29  7:56                             ` Jeff King
  0 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-04-27 17:00 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Christian Couder; +Cc: Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年4月16日周四 上午2:34写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > Will correct it.
> >
> > What I am working on for reroll v13:
> >
> > * Add new commit "receive-pack: feed extended_status to post-receive“
> >
> >    When commands are fed to the "post-receive" hook, `extended_status` will
> >    be parsed and the real old-oid, new-oid, reference name will feed to
> >    the "post-receive" hook.
> >
> > * Add test cases for "git push --porcelain".
> >
> >    I found some changes not covered by test.  So I decide add some
> >    test cases for "git push --porcelain".  I will split the test cases into
> >    multiple files inside "t5411/" like:
> >
> >        t5411/test-0000-normal-push.sh
> >        t5411/test-0001-normal-push--porcelain.sh
> >
> > * Other minor fixes.
>
> Thanks for a heads-up, but given that nobody seems to be reading
> your patches and commenting on them, you might want to slow down a
> bit.

For over a week, I have not received more comments on reroll v13 on
this topic ("jx/proc-receive-hook"). Therefore, I invite Peff and
Couder to review because I think it would be very interesting to add a
centralized workflow to Github and Gitlab.

 * [PATCH v13 3/8]:
https://public-inbox.org/git/20200418160334.15631-4-worldhello.net@gmail.com/

This patch introduces a new hook "proc-receive" on the server side. It
won't break anything except GitHub's "spokes" architecture (I guess).
Because in Alibaba, we have such issue when we implement our own
"spokes" architecture.  In order to make this special push operation
(on a special ref such as "refs/for/master/topic") idempotently on
multiple replicas, we extended the protocol of "receive-pack" and let
"spokes" to send a request with a flag, which specifies one replica as
master replica to execute the "proc-receive" hook.

* [PATCH v13 4/8]:
https://public-inbox.org/git/20200418160334.15631-5-worldhello.net@gmail.com/
* [PATCH v13 5/8]:
https://public-inbox.org/git/20200418160334.15631-6-worldhello.net@gmail.com/

Patch 4/8 and patch 5/8 extends status report for both server and
client sides. The extension is backward compatible. When user push
like this:

    git push origin HEAD:refs/for/master/topic

Old version of git may report like:

    To <URL/of/upstream.git>
     * [new reference] HEAD -> refs/for/master/topic

But new version of git may report like:

    To <URL/of/upstream.git>
     + <OID-B>...<OID-A> HEAD -> refs/pull/123/head (forced update)


As for how to use this "proc-receive" hook to implement a centralized
git workflow, please refer to my blog as a reference:

* https://git-repo.info/en/2020/03/agit-flow-and-git-repo/

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-27 17:00                           ` Jiang Xin
@ 2020-04-29  7:56                             ` Jeff King
  2020-04-30 15:33                               ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Jeff King @ 2020-04-29  7:56 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Christian Couder, Git List, Jiang Xin

On Tue, Apr 28, 2020 at 01:00:00AM +0800, Jiang Xin wrote:

> For over a week, I have not received more comments on reroll v13 on
> this topic ("jx/proc-receive-hook"). Therefore, I invite Peff and
> Couder to review because I think it would be very interesting to add a
> centralized workflow to Github and Gitlab.
> 
>  * [PATCH v13 3/8]:
> https://public-inbox.org/git/20200418160334.15631-4-worldhello.net@gmail.com/
> 
> This patch introduces a new hook "proc-receive" on the server side. It
> won't break anything except GitHub's "spokes" architecture (I guess).
> Because in Alibaba, we have such issue when we implement our own
> "spokes" architecture.  In order to make this special push operation
> (on a special ref such as "refs/for/master/topic") idempotently on
> multiple replicas, we extended the protocol of "receive-pack" and let
> "spokes" to send a request with a flag, which specifies one replica as
> master replica to execute the "proc-receive" hook.

We don't do any magic ref-rewriting like this, so we wouldn't turn such
a hook on. Presumably without such a hook, the protocol would remain the
same (i.e., if there was no rewrite, then the report-status response
wouldn't have the extra field).

And I'm not sure if you're proposing to also use it as the trigger point
to coordinate the ref update across replicas. That wouldn't be helpful
for our architecture. All Git client connections to GitHub terminate at
a proxy layer that handles load balancing, authentication, and routing.
For pushes, that's the layer that multiplexes the packfile to each
replica, and it drives the ref update through an out-of-band procedure.
So there is no master replica at all; they're all talking equally to the
proxy layer which coordinates things (and the same is true for non-push
updates; they're coordinated by whichever entity is trying to perform
the update).

We do run receive-pack on each replica backend. We have a hacky patch
for a config option that tells receive-pack to just skip the actual
ref-transaction, leaving it up to the proxy layer to do. I've been
pushing for us to actually abandon receive-pack entirely, since most of
its heavy lifting can be done by sub-programs (for-each-ref for the
advertisement, index-pack to receive the pack, and update-ref to update
refs). But it's a non-trivial change, and the benefits are only
moderate, so it hasn't quite been worth the effort yet.

So I don't really have much of an opinion on it either way.

Reading the commit message for patch 3, I do have one small comment. I
see this:

  The reporting function from "receive-pack" to "send-pack" is extended
  using a backward compatible way by adding key-value pairs after an null
  character, like:
  
      # OK, run this command successfully with optional extended-status.
      ok <reference>\0ref=refs/pull/123/head old-oid=...
  
      # NO, I reject it.
      ng <reference> <error message>

but we should probably avoid trickery like stuffing extra data after a
NUL byte. We could quite easily extend the protocol with a new
capability here. The server advertises "report-status-v2" or something,
and the client responds with the appropriate capability to indicate that
it understands. And then the new format can be something more
extensible, like:

  pktline(ref=refs/heads/master)
  pktline(status=ok)
  pktline(old-oid=...)
  pktline(rewrite=refs/pull/123/head)
  delim-pkt(0001)
  pktline(ref=refs/heads/other)
  pktline(status=error)
  pktline(message=<error message>
  flush-pkt(0000)

which would give us room for more keys later, but without worrying about
parsing issues after the NUL, running out of room in a pktline if you
have too many keys, etc.

I don't have any real thoughts on the hook interface itself. It seems
like the ok/no/ft/alt responses would allow just about any hook you'd
want. It does seems to also use NUL trickery in the early part. If we're
designing from scratch, I think we should strive for a cleaner protocol.

-Peff

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-29  7:56                             ` Jeff King
@ 2020-04-30 15:33                               ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 0/7] New proc-receive hook for centralized workflow Jiang Xin
                                                   ` (8 more replies)
  0 siblings, 9 replies; 266+ messages in thread
From: Jiang Xin @ 2020-04-30 15:33 UTC (permalink / raw)
  To: Jeff King; +Cc: Junio C Hamano, Christian Couder, Git List, Jiang Xin

Jeff King <peff@peff.net> 于2020年4月29日周三 下午3:56写道:
>
> On Tue, Apr 28, 2020 at 01:00:00AM +0800, Jiang Xin wrote:
>
> > For over a week, I have not received more comments on reroll v13 on
> > this topic ("jx/proc-receive-hook"). Therefore, I invite Peff and
> > Couder to review because I think it would be very interesting to add a
> > centralized workflow to Github and Gitlab.
> >
> >  * [PATCH v13 3/8]:
> > https://public-inbox.org/git/20200418160334.15631-4-worldhello.net@gmail.com/
> >
> > This patch introduces a new hook "proc-receive" on the server side. It
> > won't break anything except GitHub's "spokes" architecture (I guess).
> > Because in Alibaba, we have such issue when we implement our own
> > "spokes" architecture.  In order to make this special push operation
> > (on a special ref such as "refs/for/master/topic") idempotently on
> > multiple replicas, we extended the protocol of "receive-pack" and let
> > "spokes" to send a request with a flag, which specifies one replica as
> > master replica to execute the "proc-receive" hook.
>
> We don't do any magic ref-rewriting like this, so we wouldn't turn such
> a hook on. Presumably without such a hook, the protocol would remain the
> same (i.e., if there was no rewrite, then the report-status response
> wouldn't have the extra field).
>
> And I'm not sure if you're proposing to also use it as the trigger point
> to coordinate the ref update across replicas. That wouldn't be helpful
> for our architecture. All Git client connections to GitHub terminate at
> a proxy layer that handles load balancing, authentication, and routing.
> For pushes, that's the layer that multiplexes the packfile to each
> replica, and it drives the ref update through an out-of-band procedure.
> So there is no master replica at all; they're all talking equally to the
> proxy layer which coordinates things (and the same is true for non-push
> updates; they're coordinated by whichever entity is trying to perform
> the update).
>
> We do run receive-pack on each replica backend. We have a hacky patch
> for a config option that tells receive-pack to just skip the actual
> ref-transaction, leaving it up to the proxy layer to do. I've been
> pushing for us to actually abandon receive-pack entirely, since most of
> its heavy lifting can be done by sub-programs (for-each-ref for the
> advertisement, index-pack to receive the pack, and update-ref to update
> refs). But it's a non-trivial change, and the benefits are only
> moderate, so it hasn't quite been worth the effort yet.
>

Thanks to Peff for providing technical details of the architecture.  I
understand that "receive-pack" of GitHub backend is not involved in
references update (executing the commands), so the "proc-receive" hook
won't be turned on for GitHub's architecture. While in our
architecture (inspired by "spokes" of GitHub), the proxy will deliver
not only packfile, but also commands to all three replicas. The proxy
will execute "receive-pack" on the replica with a special argument, so
the proxy can talk with "receive-pack" with an extended protocol.
After running pre-receive hook and release the packfile from
quarantine, the replica will stop and wait for the proxy to
coordinate. After creating a distributed lock, the proxy will tell all
the replicas continue to update the references.  One problem we met is
the proc-receive and the post-receive hook must be executed once. We
can make the execution of the hooks idempotent, or let only one of the
replica run the hook. We choose the latter.

> So I don't really have much of an opinion on it either way.
>
> Reading the commit message for patch 3, I do have one small comment. I
> see this:
>
>   The reporting function from "receive-pack" to "send-pack" is extended
>   using a backward compatible way by adding key-value pairs after an null
>   character, like:
>
>       # OK, run this command successfully with optional extended-status.
>       ok <reference>\0ref=refs/pull/123/head old-oid=...
>
>       # NO, I reject it.
>       ng <reference> <error message>
>
> but we should probably avoid trickery like stuffing extra data after a
> NUL byte. We could quite easily extend the protocol with a new
> capability here. The server advertises "report-status-v2" or something,
> and the client responds with the appropriate capability to indicate that
> it understands. And then the new format can be something more
> extensible, like:
>
>   pktline(ref=refs/heads/master)
>   pktline(status=ok)
>   pktline(old-oid=...)
>   pktline(rewrite=refs/pull/123/head)
>   delim-pkt(0001)
>   pktline(ref=refs/heads/other)
>   pktline(status=error)
>   pktline(message=<error message>
>   flush-pkt(0000)

The above status report is from "receive-pack" to "send-pack", and
another place that we need to extend is in `print_helper_status()` of
`builtin/send-pack.c`, which will send report status in the following
format from "send-pack" to remote-helper (such as remote-curl.c):

    ok <ref>
    error <ref> <error message>

BTW, I don't know why not use "ng", but use "error" here.

I agree that adding new capability (report-status-v2) is a better
solution, but I think the above extension is a bit wordy. We add
additional 18 characters for each "ok <ref>" status, and add
additional 32 characters for each "ng <ref> <msg>" status. Can we
extend it like this:

    ok <ref>
    [optional key-value pairs]
    ng <ref> <error message>
    [optional key-value pairs]

E.g.:

    ok refs/heads/master     # no extensions
    ok refs/for/master/topic  # send 2 commits, and create 2 changes
    rewrite=refs/changes/123/1
    new-oid=...
    # use rewrite key-value pair as delimiter,
    # because plain text is in use from "send-pack" to remote-helper.
    rewrite=refs/changes/124/1
    new-oid=...
    ok refs/heads/next
    ng refs/heads/release non-fast forward

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v14 0/7] New proc-receive hook for centralized workflow
  2020-04-30 15:33                               ` Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-06 23:14                                   ` Junio C Hamano
  2020-05-05 14:41                                 ` [PATCH v14 1/7] transport: not report a non-head push as a branch Jiang Xin
                                                   ` (7 subsequent siblings)
  8 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This topic introduces a new hook named "proc-receive" for
"receive-pack".  We can use this hook to implement a Gerrit-like
centralized workflow.

## Changes since v13

1. Patch 3/7 introduced a new capability "report-status-v2" for git-push 

   Add new capability "report-status-v2" and new report protocol which is
   not backward compatible for report of git-push.
   
   If a user pushes to a pseudo-reference "refs/for/master/topic", and
   "receive-pack" creates two new references "refs/changes/23/123/1" and
   "refs/changes/24/124/1", for client without the knowledge of
   "report-status-v2", "receive-pack" will only send "ok/ng" directives in
   the report, such as:
   
       ok ref/for/master/topic
   
   But for client which has the knowledge of "report-status-v2",
   "receive-pack" will use "option" directives to report more attributes
   for the reference given by the above "ok/ng" directive.
   
       ok refs/for/master/topic
       option refname refs/changes/23/123/1
       option new-oid <new-oid>
       ok refs/for/master/topic
       option refname refs/changes/24/124/1
       option new-oid <new-oid>
   
   The client will report two new created references to the end user.

2. Patch 2/7: the "proc-receive" reports status using the same way like
   "report-status-v2" to "receive-pack".


## Range-diff v13...v14

1:  dc7f1d044d ! 1:  9a19919552 transport: not report a non-head push as a branch
    @@ t/t5411-proc-receive-hook.sh (new)
     +	git -C upstream.git config http.receivepack true &&
     +	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
     +	mv upstream.git "$upstream" &&
    ++	set_askpass user@host pass@host &&
     +	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
     +'
     +
    @@ t/t5411/test-0000-standard-git-push.sh (new)
     +# Refs of upstream : master(A)  
     +# Refs of workbench: master(A)  tags/v123
     +# git-push         : master(B)             next(A)
    -+test_expect_success "non-force git-push ($PROTOCOL)" '
    ++test_expect_success "git-push ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/heads/next \
    @@ t/t5411/test-0000-standard-git-push.sh (new)
     +# Refs of upstream : master(B)  next(A)
     +# Refs of workbench: master(A)           tags/v123
     +# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
    -+test_expect_success "force git-push ($PROTOCOL)" '
    ++test_expect_success "git-push -f ($PROTOCOL)" '
     +	git -C workbench push -f origin \
     +		refs/tags/v123 \
     +		:refs/heads/next \
    @@ t/t5411/test-0001-standard-git-push--porcelain.sh (new)
     +# Refs of upstream : master(A)  
     +# Refs of workbench: master(A)  tags/v123
     +# git-push         : master(B)             next(A)
    -+test_expect_success "non-force git-push (--porcelain, $PROTOCOL)" '
    ++test_expect_success "git-push ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/heads/next \
    @@ t/t5411/test-0001-standard-git-push--porcelain.sh (new)
     +# Refs of upstream : master(B)  next(A)
     +# Refs of workbench: master(A)           tags/v123
     +# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
    -+test_expect_success "force git-push (--porcelain, $PROTOCOL)" '
    ++test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain -f origin \
     +		refs/tags/v123 \
     +		:refs/heads/next \
    @@ t/t5411/test-0001-standard-git-push--porcelain.sh (new)
     +		git update-ref -d refs/heads/a/b/c
     +	)
     +'
    -+
     
      ## t/t5411/test-0002-pre-receive-declined.sh (new) ##
     @@
    -+test_expect_success "setup pre-receive hook (declined, $PROTOCOL)" '
    ++test_expect_success "setup pre-receive hook ($PROTOCOL)" '
     +	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
     +	write_script "$upstream/hooks/pre-receive" <<-EOF
     +	exit 1
    @@ t/t5411/test-0002-pre-receive-declined.sh (new)
     
      ## t/t5411/test-0003-pre-receive-declined--porcelain.sh (new) ##
     @@
    -+test_expect_success "setup pre-receive hook (declined, $PROTOCOL)" '
    ++test_expect_success "setup pre-receive hook ($PROTOCOL)" '
     +	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
     +	write_script "$upstream/hooks/pre-receive" <<-EOF
     +	exit 1
    @@ t/t5411/test-0003-pre-receive-declined--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git-push         : master(B)             next(A)
    -+test_expect_success "git-push is declined (--porcelain, $PROTOCOL)" '
    ++test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/heads/next \
2:  673f44599f < -:  ---------- connect: export parse_feature_value()
3:  43647b379a ! 2:  f1a3ca9cda receive-pack: add new proc-receive hook
    @@ Commit message
             # NO, I reject it.
             H: PKT-LINE(ng <ref> <reason>)
             # Fall through, let 'receive-pack' to execute it.
    -        H: PKT-LINE(ft <ref>)
    +        H: PKT-LINE(ok <ref>)
    +        H: PKT-LINE(option fall-through)
             # OK, but has an alternate reference.  The alternate reference name
    -        # is given in the third parameter, and other status can be given in
    -        # key-value pairs.
    -        H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
    -                    forced-update)
    +        # and other status can be given in options
    +        H: PKT-LINE(ok <ref>)
    +        H: PKT-LINE(option refname <refname>)
    +        H: PKT-LINE(option old-oid <old-oid>)
    +        H: PKT-LINE(option new-oid <new-oid>)
    +        H: PKT-LINE(option forced-update)
             H: ... ...
             H: flush-pkt
     
    @@ Commit message
         create/update different reference.  For example, a command for a pseudo
         reference "refs/for/master/topic" may create/update different reference
         such as "refs/pull/123/head".  The alternate reference name and other
    -    status are given in key-value pairs as extended status of the report
    -    line.
    +    status are given in option lines.
     
         The list of commands returned from "proc-receive" will replace the
         relevant commands that are sent from user to "receive-pack", and
    @@ Commit message
         other routines.  Finally, the result of the execution of these commands
         will be reported to end user.
     
    -    The reporting function from "receive-pack" to "send-pack" is extended
    -    using a backward compatible way by adding key-value pairs after an null
    -    character, like:
    -
    -        # OK, run this command successfully with optional extended-status.
    -        ok <reference>\0ref=refs/pull/123/head old-oid=...
    -
    -        # NO, I reject it.
    -        ng <reference> <error message>
    -
    -    Parsing the extended status of the report lines for "send-pack" is not
    -    implemented in this commit, so we can make a compatible test on old
    -    version of a Git client.
    +    The reporting function from "receive-pack" to "send-pack" will be
    +    extended in latter commit just like what the "proc-receive" hook reports
    +    to "receive-pack".
     
         Suggested-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
    @@ builtin/receive-pack.c: static void write_head_info(void)
     +#define RUN_PROC_RECEIVE_RETURNED	2
      struct command {
      	struct command *next;
    - 	const char *error_string;
    -+	const char *extended_status;
    +-	const char *error_string;
    ++	struct ref_push_report report;
      	unsigned int skip_update:1,
     -		     did_not_exist:1;
     +		     did_not_exist:1,
    @@ builtin/receive-pack.c: static void write_head_info(void)
      	int index;
      	struct object_id old_oid;
      	struct object_id new_oid;
    +@@ builtin/receive-pack.c: static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
    + 	struct command *cmd = state->cmd;
    + 
    + 	while (cmd &&
    +-	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
    ++	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
    + 		cmd = cmd->next;
    + 	if (!cmd)
    + 		return -1; /* EOF */
     @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
      	return finish_command(&proc);
      }
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +{
     +	struct command *cmd;
     +	struct command *hint = NULL;
    -+	struct strbuf extended_status = STRBUF_INIT;
     +	int code = 0;
    ++	int new_options = 1;
     +
     +	for (;;) {
     +		struct object_id old_oid, new_oid;
    -+		const char *status;
    -+		char *refname;
    -+		char *remains = NULL;
    ++		const char *head;
    ++		const char *refname;
    ++		char *p;
     +
     +		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
     +			break;
    -+		status = reader->line;
    -+		refname = strchr(status, ' ');
    -+		if (refname) {
    -+			*refname++ = '\0';
    -+			remains = strchr(refname, ' ');
    -+			if (remains)
    -+				*remains++ = '\0';
    ++
    ++		head = reader->line;
    ++		p = strchr(head, ' ');
    ++		if (!p) {
    ++			strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head);
    ++			code = -1;
    ++			continue;
     +		}
    -+		if (!refname || strlen(refname) == 0) {
    -+			strbuf_addf(errmsg, "proc-receive expected '<status> <ref> ...', got '%s'\n",
    -+				    reader->line);
    ++		*p++ = '\0';
    ++		if (!strcmp(head, "option")) {
    ++			struct ref_push_report_options *options;
    ++			const char *key, *val;
    ++
    ++			if (!hint) {
    ++			       if (new_options) {
    ++					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
    ++					new_options = 0;
    ++				}
    ++				code = -1;
    ++				continue;
    ++			}
    ++			options = hint->report.options;
    ++			while (options && options->next)
    ++				options = options->next;
    ++			if (new_options) {
    ++				if (!options) {
    ++					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
    ++					options = hint->report.options;
    ++				} else {
    ++					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
    ++					options = options->next;
    ++				}
    ++				new_options = 0;
    ++			}
    ++			assert(options);
    ++			key = p;
    ++			p = strchr(key, ' ');
    ++			if (p)
    ++				*p++ = '\0';
    ++			val = p;
    ++			if (!strcmp(key, "refname"))
    ++				options->ref_name = xstrdup_or_null(val);
    ++			else if (!strcmp(key, "old-oid") && val &&
    ++				 !parse_oid_hex(val, &old_oid, &val))
    ++				options->old_oid = oiddup(&old_oid);
    ++			else if (!strcmp(key, "new-oid") && val &&
    ++				 !parse_oid_hex(val, &new_oid, &val))
    ++				options->new_oid = oiddup(&new_oid);
    ++			else if (!strcmp(key, "forced-update"))
    ++				options->forced_update = 1;
    ++			else if (!strcmp(key, "fall-through"))
    ++				/* Fall through, let 'receive-pack' to execute it. */
    ++				hint->run_proc_receive = 0;
    ++			continue;
    ++		}
    ++
    ++		refname = p;
    ++		p = strchr(refname, ' ');
    ++		if (p)
    ++			*p++ = '\0';
    ++		if (strcmp(head, "ok") && strcmp(head, "ng")) {
    ++			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
    ++				    head, refname);
     +			code = -1;
     +			continue;
     +		}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		if (!hint) {
     +			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
     +				    refname);
    ++			code = -1;
     +			continue;
     +		}
     +		if (!hint->run_proc_receive) {
     +			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
     +				    refname);
    ++			code = -1;
     +			continue;
     +		}
    -+		if (!strcmp(status, "ng")) {
    -+			if (remains)
    -+				hint->error_string = xstrdup(remains);
    ++		if (!strcmp(head, "ng")) {
    ++			if (p)
    ++				hint->report.error_message = xstrdup(p);
     +			else
    -+				hint->error_string = "failed";
    ++				hint->report.error_message = "failed";
     +			code = -1;
    -+		} else if (!strcmp("ft", status)) {
    -+			/* Reset "run_proc_receive" field, and continue to run in "receive-pack" */
    -+			hint->run_proc_receive = 0;
    -+		} else if (!strcmp("alt", status)) {
    -+			char *alt_ref = NULL;
    -+			char *p1, *p2;
    -+
    -+			if (!remains) {
    -+				strbuf_addf(errmsg, "proc-receive expected 'alt <ref> <alt-ref> ...', got 'alt %s'\n",
    -+					    refname);
    -+				continue;
    -+			}
    -+			p1 = strchr(remains, '=');
    -+			p2 = strchr(remains, ' ');
    -+			if (!p1) {
    -+				/*
    -+				 * "alt <ref> <alt-ref>", or
    -+				 * "alt <ref> <alt-ref> forced-update"
    -+				 */
    -+				alt_ref = remains;
    -+				remains = p2;
    -+				if (remains)
    -+					*remains++ = '\0';
    -+			} else if (!p2) {
    -+				/* "alt <ref> old-oid=<oid>" */
    -+				alt_ref = NULL;
    -+			} else if (p1 > p2) {
    -+				/* "alt <ref> <alt-ref> old-oid=<oid>" */
    -+				alt_ref = remains;
    -+				remains = p2;
    -+				if (remains)
    -+					*remains++ = '\0';
    -+			} else {
    -+				/* "alt <ref> old-oid=<oid1> new-oid=<oid2>" */
    -+				alt_ref = NULL;
    -+			}
    -+
    -+			if (hint->extended_status)
    -+				strbuf_addstr(&extended_status, hint->extended_status);
    -+
    -+			/*
    -+			 * Already received an "ok <ref>" response, add an
    -+			 * additional "ref=<ref>" field.
    -+			 */
    -+			if (extended_status.len == 0 &&
    -+			    hint->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)
    -+				strbuf_addf(&extended_status, "ref=%s", refname);
    -+
    -+			if (alt_ref)
    -+				strbuf_addf(&extended_status, "%sref=%s",
    -+					    extended_status.len > 0 ? " ": "",
    -+					    alt_ref);
    -+			else if (extended_status.len > 0)
    -+				/*
    -+				 * Start new extended status for refname, if it
    -+				 * is not the first report for this reference.
    -+				 */
    -+				strbuf_addf(&extended_status, " ref=%s", refname);
    -+
    -+			if (remains) {
    -+				const char *val;
    -+				int len;
    -+
    -+				val = parse_feature_value(remains, "old-oid", &len);
    -+				if (val && len > 0)
    -+					if (!parse_oid_hex(val, &old_oid, &val))
    -+						strbuf_addf(&extended_status, "%sold-oid=%s",
    -+							   extended_status.len > 0 ? " ": "",
    -+							   oid_to_hex(&old_oid));
    -+
    -+				val = parse_feature_value(remains, "new-oid", &len);
    -+				if (val && len > 0)
    -+					if (!parse_oid_hex(val, &new_oid, &val))
    -+						strbuf_addf(&extended_status, "%snew-oid=%s",
    -+							   extended_status.len > 0 ? " ": "",
    -+							   oid_to_hex(&new_oid));
    -+
    -+				if (parse_feature_request(remains, "forced-update"))
    -+					strbuf_addf(&extended_status, "%sforced-update",
    -+						    extended_status.len > 0 ? " ": "");
    -+			}
    -+
    -+			if (extended_status.len > 0) {
    -+				free((void *)hint->extended_status);
    -+				hint->extended_status = strbuf_detach(&extended_status, NULL);
    -+			}
    -+		} else if (strcmp("ok", status)) {
    -+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
    -+				    status, refname);
    -+			code = -1;
    -+			/* Skip marking it as RUN_PROC_RECEIVE_RETURNED */
    -+			continue;
     +		}
    -+
     +		if (hint->run_proc_receive)
     +			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
    ++		new_options = 1;
     +	}
     +
     +	for (cmd = commands; cmd; cmd = cmd->next)
    -+		if (cmd->run_proc_receive && !cmd->error_string &&
    ++		if (cmd->run_proc_receive && !cmd->report.error_message &&
     +		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
    -+		    cmd->error_string = "proc-receive failed to report status";
    ++		    cmd->report.error_message = "proc-receive failed to report status";
     +		    code = -1;
     +		}
    -+
     +	return code;
     +}
     +
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	struct packet_reader reader;
     +	struct strbuf cap = STRBUF_INIT;
     +	struct strbuf errmsg = STRBUF_INIT;
    -+	int pr_use_push_options = 0;
    ++	int hook_use_push_options = 0;
     +	int version = 0;
     +	int code;
     +
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +			if (linelen < reader.pktlen) {
     +				const char *feature_list = reader.line + linelen + 1;
     +				if (parse_feature_request(feature_list, "push-options"))
    -+					pr_use_push_options = 1;
    ++					hook_use_push_options = 1;
     +			}
     +		}
     +	}
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +
     +	/* Send commands */
     +	for (cmd = commands; cmd; cmd = cmd->next) {
    -+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
    ++		if (!cmd->run_proc_receive || cmd->skip_update || cmd->report.error_message)
     +			continue;
     +		packet_write_fmt(proc.in, "%s %s %s",
     +				 oid_to_hex(&cmd->old_oid),
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +	packet_flush(proc.in);
     +
     +	/* Send push options */
    -+	if (pr_use_push_options) {
    ++	if (hook_use_push_options) {
     +		struct string_list_item *item;
     +
     +		for_each_string_list_item(item, push_options)
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
      static char *refuse_unconfigured_deny_msg =
      	N_("By default, updating the current branch in a non-bare repository\n"
      	   "is denied, because it will make the index and work tree inconsistent\n"
    +@@ builtin/receive-pack.c: static void run_update_post_hook(struct command *commands)
    + 		return;
    + 
    + 	for (cmd = commands; cmd; cmd = cmd->next) {
    +-		if (cmd->error_string || cmd->did_not_exist)
    ++		if (cmd->report.error_message || cmd->did_not_exist)
    + 			continue;
    + 		if (!proc.args.argc)
    + 			argv_array_push(&proc.args, hook);
    +@@ builtin/receive-pack.c: static void check_aliased_update_internal(struct command *cmd,
    + 	if (!dst_name) {
    + 		rp_error("refusing update to broken symref '%s'", cmd->ref_name);
    + 		cmd->skip_update = 1;
    +-		cmd->error_string = "broken symref";
    ++		cmd->report.error_message = "broken symref";
    + 		return;
    + 	}
    + 	dst_name = strip_namespace(dst_name);
    +@@ builtin/receive-pack.c: static void check_aliased_update_internal(struct command *cmd,
    + 		 find_unique_abbrev(&dst_cmd->old_oid, DEFAULT_ABBREV),
    + 		 find_unique_abbrev(&dst_cmd->new_oid, DEFAULT_ABBREV));
    + 
    +-	cmd->error_string = dst_cmd->error_string =
    ++	cmd->report.error_message = dst_cmd->report.error_message =
    + 		"inconsistent aliased update";
    + }
    + 
    +@@ builtin/receive-pack.c: static void check_aliased_updates(struct command *commands)
    + 	string_list_sort(&ref_list);
    + 
    + 	for (cmd = commands; cmd; cmd = cmd->next) {
    +-		if (!cmd->error_string)
    ++		if (!cmd->report.error_message)
    + 			check_aliased_update(cmd, &ref_list);
    + 	}
    + 
    +@@ builtin/receive-pack.c: static void set_connectivity_errors(struct command *commands,
    + 				     &opt))
    + 			continue;
    + 
    +-		cmd->error_string = "missing necessary objects";
    ++		cmd->report.error_message = "missing necessary objects";
    + 	}
    + }
    + 
    +@@ builtin/receive-pack.c: static void reject_updates_to_hidden(struct command *commands)
    + 	prefix_len = refname_full.len;
    + 
    + 	for (cmd = commands; cmd; cmd = cmd->next) {
    +-		if (cmd->error_string)
    ++		if (cmd->report.error_message)
    + 			continue;
    + 
    + 		strbuf_setlen(&refname_full, prefix_len);
    +@@ builtin/receive-pack.c: static void reject_updates_to_hidden(struct command *commands)
    + 		if (!ref_is_hidden(cmd->ref_name, refname_full.buf))
    + 			continue;
    + 		if (is_null_oid(&cmd->new_oid))
    +-			cmd->error_string = "deny deleting a hidden ref";
    ++			cmd->report.error_message = "deny deleting a hidden ref";
    + 		else
    +-			cmd->error_string = "deny updating a hidden ref";
    ++			cmd->report.error_message = "deny updating a hidden ref";
    + 	}
    + 
    + 	strbuf_release(&refname_full);
    +@@ builtin/receive-pack.c: static void reject_updates_to_hidden(struct command *commands)
    + 
    + static int should_process_cmd(struct command *cmd)
    + {
    +-	return !cmd->error_string && !cmd->skip_update;
    ++	return !cmd->report.error_message && !cmd->skip_update;
    + }
    + 
    + static void warn_if_skipped_connectivity_check(struct command *commands,
     @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command *commands,
      	struct strbuf err = STRBUF_INIT;
      
    @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command *
      			continue;
      
      		transaction = ref_transaction_begin(&err);
    + 		if (!transaction) {
    + 			rp_error("%s", err.buf);
    + 			strbuf_reset(&err);
    +-			cmd->error_string = "transaction failed to start";
    ++			cmd->report.error_message = "transaction failed to start";
    + 			continue;
    + 		}
    + 
    +-		cmd->error_string = update(cmd, si);
    ++		cmd->report.error_message = update(cmd, si);
    + 
    +-		if (!cmd->error_string
    ++		if (!cmd->report.error_message
    + 		    && ref_transaction_commit(transaction, &err)) {
    + 			rp_error("%s", err.buf);
    + 			strbuf_reset(&err);
    +-			cmd->error_string = "failed to update ref";
    ++			cmd->report.error_message = "failed to update ref";
    + 		}
    + 		ref_transaction_free(transaction);
    + 	}
     @@ builtin/receive-pack.c: static void execute_commands_atomic(struct command *commands,
      	}
      
    @@ builtin/receive-pack.c: static void execute_commands_atomic(struct command *comm
     +		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
      			continue;
      
    - 		cmd->error_string = update(cmd, si);
    +-		cmd->error_string = update(cmd, si);
    ++		cmd->report.error_message = update(cmd, si);
    + 
    +-		if (cmd->error_string)
    ++		if (cmd->report.error_message)
    + 			goto failure;
    + 	}
    + 
    +@@ builtin/receive-pack.c: static void execute_commands_atomic(struct command *commands,
    + 
    + failure:
    + 	for (cmd = commands; cmd; cmd = cmd->next)
    +-		if (!cmd->error_string)
    +-			cmd->error_string = reported_error;
    ++		if (!cmd->report.error_message)
    ++			cmd->report.error_message = reported_error;
    + 
    + cleanup:
    + 	ref_transaction_free(transaction);
     @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      	struct iterate_data data;
      	struct async muxer;
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      
      	if (unpacker_error) {
      		for (cmd = commands; cmd; cmd = cmd->next)
    +-			cmd->error_string = "unpacker error";
    ++			cmd->report.error_message = "unpacker error";
    + 		return;
    + 	}
    + 
     @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      
      	reject_updates_to_hidden(commands);
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +
      	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
      		for (cmd = commands; cmd; cmd = cmd->next) {
    - 			if (!cmd->error_string)
    +-			if (!cmd->error_string)
    +-				cmd->error_string = "pre-receive hook declined";
    ++			if (!cmd->report.error_message)
    ++				cmd->report.error_message = "pre-receive hook declined";
    + 		}
    + 		return;
    + 	}
    +@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
    + 	 */
    + 	if (tmp_objdir_migrate(tmp_objdir) < 0) {
    + 		for (cmd = commands; cmd; cmd = cmd->next) {
    +-			if (!cmd->error_string)
    +-				cmd->error_string = "unable to migrate objects to permanent storage";
    ++			if (!cmd->report.error_message)
    ++				cmd->report.error_message = "unable to migrate objects to permanent storage";
    + 		}
    + 		return;
    + 	}
     @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
      	free(head_name_to_free);
      	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +	if (run_proc_receive &&
     +	    run_proc_receive_hook(commands, push_options))
     +		for (cmd = commands; cmd; cmd = cmd->next)
    -+			if (!cmd->error_string  && (cmd->run_proc_receive || use_atomic))
    -+				cmd->error_string = "fail to run proc-receive hook";
    ++			if (!cmd->report.error_message  && (cmd->run_proc_receive || use_atomic))
    ++				cmd->report.error_message = "fail to run proc-receive hook";
     +
      	if (use_atomic)
      		execute_commands_atomic(commands, si);
      	else
    +@@ builtin/receive-pack.c: static void update_shallow_info(struct command *commands,
    + 		if (is_null_oid(&cmd->new_oid))
    + 			continue;
    + 		if (ref_status[cmd->index]) {
    +-			cmd->error_string = "shallow update not allowed";
    ++			cmd->report.error_message = "shallow update not allowed";
    + 			cmd->skip_update = 1;
    + 		}
    + 	}
     @@ builtin/receive-pack.c: static void report(struct command *commands, const char *unpack_status)
      	packet_buf_write(&buf, "unpack %s\n",
      			 unpack_status ? unpack_status : "ok");
      	for (cmd = commands; cmd; cmd = cmd->next) {
     -		if (!cmd->error_string)
    --			packet_buf_write(&buf, "ok %s\n",
    ++		if (!cmd->report.error_message)
    + 			packet_buf_write(&buf, "ok %s\n",
     -					 cmd->ref_name);
    --		else
    --			packet_buf_write(&buf, "ng %s %s\n",
    --					 cmd->ref_name, cmd->error_string);
    -+		if (!cmd->error_string) {
    -+			if (cmd->extended_status)
    -+				packet_buf_write(&buf, "ok %s%c%s\n",
    -+						 cmd->ref_name, '\0',
    -+						 cmd->extended_status);
    -+			else
    -+				packet_buf_write(&buf, "ok %s\n",
     +						 cmd->ref_name);
    -+		} else {
    -+			if (cmd->extended_status)
    -+				packet_buf_write(&buf, "ng %s %s%c%s\n",
    -+						 cmd->ref_name, cmd->error_string,
    -+						 '\0', cmd->extended_status);
    -+			else
    -+				packet_buf_write(&buf, "ng %s %s\n",
    -+						 cmd->ref_name, cmd->error_string);
    -+		}
    + 		else
    + 			packet_buf_write(&buf, "ng %s %s\n",
    +-					 cmd->ref_name, cmd->error_string);
    ++					 cmd->ref_name, cmd->report.error_message);
      	}
      	packet_buf_flush(&buf);
      
    +@@ builtin/receive-pack.c: int cmd_receive_pack(int argc, const char **argv, const char *prefix)
    + 		if (!check_cert_push_options(&push_options)) {
    + 			struct command *cmd;
    + 			for (cmd = commands; cmd; cmd = cmd->next)
    +-				cmd->error_string = "inconsistent push options";
    ++				cmd->report.error_message = "inconsistent push options";
    + 		}
    + 
    + 		prepare_shallow_info(&si, &shallow);
    +
    + ## remote.h ##
    +@@ remote.h: int for_each_remote(each_remote_fn fn, void *priv);
    + 
    + int remote_has_url(struct remote *remote, const char *url);
    + 
    ++struct ref_push_report_options {
    ++	char *ref_name;
    ++	struct object_id *old_oid;
    ++	struct object_id *new_oid;
    ++	unsigned int forced_update:1;
    ++	struct ref_push_report_options *next;
    ++};
    ++
    ++struct ref_push_report {
    ++	const char *message;
    ++	const char *error_message;
    ++	struct ref_push_report_options *options;
    ++};
    ++
    + struct ref {
    + 	struct ref *next;
    + 	struct object_id old_oid;
     
      ## t/helper/test-proc-receive.c (new) ##
     @@
    @@ t/t5411/test-0012-no-hook-error--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       next(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: no hook, fail to push special ref (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:next \
     +		HEAD:refs/for/master/topic \
    @@ t/t5411/test-0012-no-hook-error--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: no hook, all failed for atomic push (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain --atomic origin \
     +		$B:master \
     +		HEAD:next \
    @@ t/t5411/test-0013-bad-protocol.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok
    -+	remote: error: proc-receive expected "<status> <ref> ...", got "ok"
    ++	remote: error: proc-receive reported incomplete status line: "ok"
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: bad protocol (unknown version, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       next(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: bad protocol (no report, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/heads/next \
     +		HEAD:refs/for/master/topic >out 2>&1 &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: bad protocol (no ref, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic\
     +		>out 2>&1 &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok
    -+	remote: error: proc-receive expected "<status> <ref> ...", got "ok"
    ++	remote: error: proc-receive reported incomplete status line: "ok"
     +	To <URL/of/upstream.git>
     +	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
     +	Done
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: bad protocol (unknown status, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +			HEAD:refs/for/master/topic \
     +			>out 2>&1 &&
    @@ t/t5411/test-0021-report-ng--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: fail to update (ng, no message, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0021-report-ng--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: fail to update (ng, with message, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0023-report-unexpect-ref--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         : (B)                   refs/for/master/topic
    -+test_expect_success "proc-receive: report unexpected ref (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/for/master/topic \
    @@ t/t5411/test-0025-report-unknown-ref--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/a/b/c/my/topic
    -+test_expect_success "proc-receive: report unknown reference (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/a/b/c/my/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0027-push-options--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -o ...  :                       refs/for/master/topic
    -+test_expect_success "proc-receive: not support push options (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push \
     +		--porcelain \
     +		-o issue=123 \
    @@ t/t5411/test-0027-push-options--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -o ...  :                       next(A)  refs/for/master/topic
    -+test_expect_success "proc-receive: push with options (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
     +	git -C workbench push \
     +		--porcelain \
     +		--atomic \
    @@ t/t5411/test-0031-report-ok--porcelain.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic
    -+test_expect_success "proc-receive: ok (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0031-report-ok--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     
    - ## t/t5411/test-0032-report-alt.sh (new) ##
    + ## t/t5411/test-0032-report-with-options.sh (new) ##
     @@
    -+test_expect_success "setup proc-receive hook (alt <ref>, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic"
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option old-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref>, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic
    -+	remote: error: proc-receive expected "alt <ref> <alt-ref> ...", got "alt refs/for/master/topic"
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
     +	To <URL/of/upstream.git>
     +	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref>, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option refname, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/pull/123/head"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> forced-update, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/pull/123/head forced-update"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option forced-update"
     +	EOF
     +'
    -+
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option forced-update
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> old-oid=X, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option old-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic old-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X new-oid=Y, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (with multiple alt reports, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
     +		-r "ok refs/for/a/b/c/topic" \
    -+		-r "alt refs/for/next/topic refs/pull/123/head" \
    -+		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
    ++		-r "ok refs/for/next/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/124/head" \
    ++		-r "option old-oid $B" \
    ++		-r "option forced-update" \
    ++		-r "option new-oid $A"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: with multiple alt reports ($PROTOCOL)" '
    ++test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/next/topic \
     +		HEAD:refs/for/a/b/c/topic \
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok refs/for/a/b/c/topic
    -+	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
    ++	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/124/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: proc-receive> option forced-update
    ++	remote: proc-receive> option new-oid <COMMIT-A>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    @@ t/t5411/test-0032-report-alt.sh (new)
     +	test_cmp expect actual
     +'
     
    - ## t/t5411/test-0033-report-alt--porcelain.sh (new) ##
    + ## t/t5411/test-0033-report-with-options--porcelain.sh (new) ##
     @@
    -+test_expect_success "setup proc-receive hook (alt <ref>, --porcelain, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic"
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option old-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref>, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic
    -+	remote: error: proc-receive expected "alt <ref> <alt-ref> ...", got "alt refs/for/master/topic"
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
     +	To <URL/of/upstream.git>
     +	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
     +	Done
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref>, --porcelain, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option refname, $PROTOCOL/porcelain)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/pull/123/head"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref>, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> forced-update, --porcelain, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/pull/123/head forced-update"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option forced-update"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> forced-update, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head forced-update
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option forced-update
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> <alt-ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option old-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> <alt-ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic old-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (alt <ref> old-oid=X new-oid=Y, --porcelain, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL/porcelain)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report alt (alt <ref> old-oid=X new-oid=Y, --porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (with multiple alt reports, --porcelain, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL/porcelain)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
     +		-r "ok refs/for/a/b/c/topic" \
    -+		-r "alt refs/for/next/topic refs/pull/123/head" \
    -+		-r "alt refs/for/master/topic refs/pull/124/head old-oid=$B forced-update new-oid=$A"
    ++		-r "ok refs/for/next/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/124/head" \
    ++		-r "option old-oid $B" \
    ++		-r "option forced-update" \
    ++		-r "option new-oid $A"
    ++
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: with multiple alt reports (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/next/topic \
     +		HEAD:refs/for/a/b/c/topic \
    @@ t/t5411/test-0033-report-alt--porcelain.sh (new)
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok refs/for/a/b/c/topic
    -+	remote: proc-receive> alt refs/for/next/topic refs/pull/123/head
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/124/head old-oid=<COMMIT-B> forced-update new-oid=<COMMIT-A>
    ++	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/124/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: proc-receive> option forced-update
    ++	remote: proc-receive> option new-oid <COMMIT-A>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
    @@ t/t5411/test-0034-report-ft.sh (new)
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ft refs/for/master/topic"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option fall-through"
     +	EOF
     +'
     +
    @@ t/t5411/test-0034-report-ft.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    -+	remote: proc-receive> ft refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option fall-through
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0034-report-ft.sh (new)
     
      ## t/t5411/test-0035-report-ft--porcelain.sh (new) ##
     @@
    -+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (fall-through, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ft refs/for/master/topic"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option fall-through"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(B)
    -+test_expect_success "proc-receive: fall throught, let receive-pack to execute (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		$B:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0035-report-ft--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
    -+	remote: proc-receive> ft refs/for/master/topic
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option fall-through
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0035-report-ft--porcelain.sh (new)
     +	git -C "$upstream" update-ref -d refs/for/master/topic
     +'
     
    - ## t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new) ##
    + ## t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh (new) ##
     @@
    -+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 1st resp, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    -+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$A new-oid=$B"
    -+	EOF
    -+'
    -+
    -+# Refs of upstream : master(A)
    -+# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st resp ($PROTOCOL)" '
    -+	git -C workbench push origin \
    -+		HEAD:refs/for/master/topic \
    -+		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	To <URL/of/upstream.git>
    -+	 * [new reference] HEAD -> refs/for/master/topic
    -+	EOF
    -+	test_cmp expect actual &&
    -+	git -C "$upstream" show-ref >out &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	<COMMIT-A> refs/heads/master
    -+	EOF
    -+	test_cmp expect actual
    -+'
    -+
    -+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 2nd resp, $PROTOCOL)" '
    -+	write_script "$upstream/hooks/proc-receive" <<-EOF
    -+	printf >&2 "# proc-receive hook\n"
    -+	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    -+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    -+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$B new-oid=$A forced-update"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/24/124/1" \
    ++		-r "option old-oid $ZERO_OID" \
    ++		-r "option new-oid $A" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/25/125/1" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd resp ($PROTOCOL)" '
    ++test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/24/124/1
    ++	remote: proc-receive> option old-oid <ZERO-OID>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/25/125/1
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (ok and alt for the same ref, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
     +		-r "ok refs/for/master/topic" \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/1 new-oid=$B old-oid=$A"
    ++		-r "option refname refs/changes/24/124/1" \
    ++		-r "option old-oid $ZERO_OID" \
    ++		-r "option new-oid $A" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/25/125/1" \
    ++		-r "option old-oid $B" \
    ++		-r "option new-oid $A" \
    ++		-r "option forced-update"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report ok and alt for the same ref ($PROTOCOL)" '
    ++test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
    ++	remote: proc-receive> option refname refs/changes/24/124/1
    ++	remote: proc-receive> option old-oid <ZERO-OID>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/25/125/1
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> option forced-update
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (multiple responses, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/changes/23/123/1" \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/23/123/1" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/24/124/2" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report multiple response ($PROTOCOL)" '
    ++test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/23/123/1
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/24/124/2
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0036-report-multi-alt-for-one-ref.sh (new)
     +	test_cmp expect actual
     +'
     
    - ## t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh (new) ##
    + ## t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh (new) ##
     @@
    -+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 1st resp, $PROTOCOL)" '
    -+	write_script "$upstream/hooks/proc-receive" <<-EOF
    -+	printf >&2 "# proc-receive hook\n"
    -+	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    -+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$A new-oid=$B"
    -+	EOF
    -+'
    -+
    -+# Refs of upstream : master(A)
    -+# Refs of workbench: master(A)  tags/v123
    -+# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 1st resp (--porcelain, $PROTOCOL)" '
    -+	git -C workbench push --porcelain origin \
    -+		HEAD:refs/for/master/topic \
    -+		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: # post-receive hook
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	To <URL/of/upstream.git>
    -+	*    HEAD:refs/for/master/topic    [new reference]
    -+	Done
    -+	EOF
    -+	test_cmp expect actual &&
    -+	git -C "$upstream" show-ref >out &&
    -+	make_user_friendly_and_stable_output <out >actual &&
    -+	cat >expect <<-EOF &&
    -+	<COMMIT-A> refs/heads/master
    -+	EOF
    -+	test_cmp expect actual
    -+'
    -+
    -+test_expect_success "setup proc-receive hook (multiple alt, no alt-ref for the 2nd resp, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/1 old-oid=$ZERO_OID new-oid=$A" \
    -+		-r "alt refs/for/master/topic old-oid=$A new-oid=$B" \
    -+		-r "alt refs/for/master/topic refs/changes/25/125/1 old-oid=$B new-oid=$A forced-update"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/24/124/1" \
    ++		-r "option old-oid $ZERO_OID" \
    ++		-r "option new-oid $A" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/25/125/1" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report multiple alt, no alt-ref for the 2nd resp (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 old-oid=<ZERO-OID> new-oid=<COMMIT-A>
    -+	remote: proc-receive> alt refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/25/125/1 old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/24/124/1
    ++	remote: proc-receive> option old-oid <ZERO-OID>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/25/125/1
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (ok and alt for the same ref, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
     +		-r "ok refs/for/master/topic" \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/1 new-oid=$B old-oid=$A"
    ++		-r "option refname refs/changes/24/124/1" \
    ++		-r "option old-oid $ZERO_OID" \
    ++		-r "option new-oid $A" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/25/125/1" \
    ++		-r "option old-oid $B" \
    ++		-r "option new-oid $A" \
    ++		-r "option forced-update"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report ok and alt for the same ref (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh (new)
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/1 new-oid=<COMMIT-B> old-oid=<COMMIT-A>
    ++	remote: proc-receive> option refname refs/changes/24/124/1
    ++	remote: proc-receive> option old-oid <ZERO-OID>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/25/125/1
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> option forced-update
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh (new)
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success "setup proc-receive hook (multiple responses, $PROTOCOL)" '
    ++test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "alt refs/for/master/topic refs/changes/23/123/1" \
    -+		-r "alt refs/for/master/topic refs/changes/24/124/2 old-oid=$A new-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/23/123/1" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/changes/24/124/2" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         :                       refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report multiple response (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		HEAD:refs/for/master/topic \
     +		>out 2>&1 &&
    @@ t/t5411/test-0037-report-multi-alt-for-one-ref--porcelain.sh (new)
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: # proc-receive hook
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/23/123/1
    -+	remote: proc-receive> alt refs/for/master/topic refs/changes/24/124/2 old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/23/123/1
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/changes/24/124/2
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
    @@ t/t5411/test-0038-report-mixed-refs.sh (new)
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
     +		-r "ok refs/for/next/topic" \
    -+		-r "alt refs/for/master/topic refs/for/master/topic old-oid=$A new-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
    @@ t/t5411/test-0038-report-mixed-refs.sh (new)
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok refs/for/next/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
    @@ t/t5411/test-0038-report-mixed-refs.sh (new)
     +		git update-ref -d refs/heads/bar &&
     +		git update-ref -d refs/heads/baz
     +	)
    -+
     +'
     
      ## t/t5411/test-0039-report-mixed-refs--porcelain.sh (new) ##
    @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh (new)
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
     +		-r "ok refs/for/next/topic" \
    -+		-r "alt refs/for/master/topic refs/for/master/topic old-oid=$A new-oid=$B"
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/for/master/topic" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B"
     +	EOF
     +'
     +
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
    -+test_expect_success "proc-receive: report update of mixed refs (--porcelain, $PROTOCOL)" '
    ++test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/heads/bar \
    @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh (new)
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive> ok refs/for/next/topic
    -+	remote: proc-receive> alt refs/for/master/topic refs/for/master/topic old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/for/master/topic
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
4:  d01004ff52 < -:  ---------- send-pack: extension for client-side status report
5:  583b5bdfcd < -:  ---------- receive-pack: feed extended_status to post-receive
-:  ---------- > 3:  267da25afd New capability "report-status-v2" for git-push
-:  ---------- > 4:  2cc92d9383 receive-pack: feed report options to post-receive
6:  860be91506 = 5:  47c5e59c9a refs.c: refactor to reuse ref_is_hidden()
7:  9489db75e6 ! 6:  37e041156f receive-pack: new config receive.procReceiveRefs
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +	if (proc_receive_refs.nr > 0) {
     +		struct strbuf refname_full = STRBUF_INIT;
     +		size_t prefix_len;
    ++
    ++		strbuf_addstr(&refname_full, get_git_namespace());
    ++		prefix_len = refname_full.len;
      
     -		/* TODO: replace the fixed prefix by looking up git config variables. */
     -		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
     -			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
     -			run_proc_receive = 1;
    -+		strbuf_addstr(&refname_full, get_git_namespace());
    -+		prefix_len = refname_full.len;
    -+
     +		for (cmd = commands; cmd; cmd = cmd->next) {
     +			if (!should_process_cmd(cmd))
     +				continue;
    @@ t/t5411/test-0040-process-all-refs.sh (new)
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ft refs/heads/master" \
    -+		-r "ft refs/heads/foo" \
    -+		-r "ft refs/heads/bar" \
    -+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$A new-oid=$B " \
    -+		-r "alt refs/for/next/topic refs/pull/124/head old-oid=$B new-oid=$A forced-update"
    ++		-r "ok refs/heads/master" \
    ++		-r "option fall-through" \
    ++		-r "ok refs/heads/foo" \
    ++		-r "option fall-through" \
    ++		-r "ok refs/heads/bar" \
    ++		-r "option fall-through" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B" \
    ++		-r "ok refs/for/next/topic" \
    ++		-r "option refname refs/pull/124/head" \
    ++		-r "option old-oid $B" \
    ++		-r "option new-oid $A" \
    ++		-r "option forced-update"
     +	EOF
     +'
     +
    @@ t/t5411/test-0040-process-all-refs.sh (new)
     +	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: proc-receive> ft refs/heads/master
    -+	remote: proc-receive> ft refs/heads/foo
    -+	remote: proc-receive> ft refs/heads/bar
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: proc-receive> alt refs/for/next/topic refs/pull/124/head old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
    ++	remote: proc-receive> ok refs/heads/master
    ++	remote: proc-receive> option fall-through
    ++	remote: proc-receive> ok refs/heads/foo
    ++	remote: proc-receive> option fall-through
    ++	remote: proc-receive> ok refs/heads/bar
    ++	remote: proc-receive> option fall-through
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
    ++	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive> option refname refs/pull/124/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> option forced-update
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
     +	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
    @@ t/t5411/test-0041-process-all-refs--porcelain.sh (new)
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ft refs/heads/master" \
    -+		-r "ft refs/heads/foo" \
    -+		-r "ft refs/heads/bar" \
    -+		-r "alt refs/for/master/topic refs/pull/123/head old-oid=$A new-oid=$B " \
    -+		-r "alt refs/for/next/topic refs/pull/124/head old-oid=$B new-oid=$A forced-update"
    ++		-r "ok refs/heads/master" \
    ++		-r "option fall-through" \
    ++		-r "ok refs/heads/foo" \
    ++		-r "option fall-through" \
    ++		-r "ok refs/heads/bar" \
    ++		-r "option fall-through" \
    ++		-r "ok refs/for/master/topic" \
    ++		-r "option refname refs/pull/123/head" \
    ++		-r "option old-oid $A" \
    ++		-r "option new-oid $B" \
    ++		-r "ok refs/for/next/topic" \
    ++		-r "option refname refs/pull/124/head" \
    ++		-r "option old-oid $B" \
    ++		-r "option new-oid $A" \
    ++		-r "option forced-update"
     +	EOF
     +'
     +
     +# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
     +# Refs of workbench: master(A)  tags/v123
     +# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
    -+test_expect_success "proc-receive: process all refs (--porcelain) ($PROTOCOL)" '
    ++test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
     +	git -C workbench push --porcelain -f origin \
     +		HEAD:refs/heads/master \
     +		:refs/heads/foo \
    @@ t/t5411/test-0041-process-all-refs--porcelain.sh (new)
     +	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    -+	remote: proc-receive> ft refs/heads/master
    -+	remote: proc-receive> ft refs/heads/foo
    -+	remote: proc-receive> ft refs/heads/bar
    -+	remote: proc-receive> alt refs/for/master/topic refs/pull/123/head old-oid=<COMMIT-A> new-oid=<COMMIT-B>
    -+	remote: proc-receive> alt refs/for/next/topic refs/pull/124/head old-oid=<COMMIT-B> new-oid=<COMMIT-A> forced-update
    ++	remote: proc-receive> ok refs/heads/master
    ++	remote: proc-receive> option fall-through
    ++	remote: proc-receive> ok refs/heads/foo
    ++	remote: proc-receive> option fall-through
    ++	remote: proc-receive> ok refs/heads/bar
    ++	remote: proc-receive> option fall-through
    ++	remote: proc-receive> ok refs/for/master/topic
    ++	remote: proc-receive> option refname refs/pull/123/head
    ++	remote: proc-receive> option old-oid <COMMIT-A>
    ++	remote: proc-receive> option new-oid <COMMIT-B>
    ++	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive> option refname refs/pull/124/head
    ++	remote: proc-receive> option old-oid <COMMIT-B>
    ++	remote: proc-receive> option new-oid <COMMIT-A>
    ++	remote: proc-receive> option forced-update
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
     +	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
8:  187a723d71 ! 7:  5e08df63d2 doc: add documentation for the proc-receive hook
    @@ Documentation/githooks.txt: The default 'update' hook, when enabled--and with
      `hooks.allowunannotated` config option unset or set to false--prevents
      unannotated tags to be pushed.
      
    ++[[proc-receive]]
     +proc-receive
     +~~~~~~~~~~~~
     +
    @@ Documentation/githooks.txt: The default 'update' hook, when enabled--and with
     +    # NO, I reject it.
     +    H: PKT-LINE(ng <ref> <reason>)
     +    # Fall through, let 'receive-pack' to execute it.
    -+    H: PKT-LINE(ft <ref>)
    ++    H: PKT-LINE(ok <ref>)
    ++    H: PKT-LINE(option fall-through)
     +    # OK, but has an alternate reference.  The alternate reference name
    -+    # is given in the third parameter, and other status can be given in
    -+    # key-value pairs.
    -+    H: PKT-LINE(alt <ref> <alt-ref> old-oid=<old-oid> new-oid=<new-oid>
    -+                forced-update)
    ++    # and other status can be given in options
    ++    H: PKT-LINE(ok <ref>)
    ++    H: PKT-LINE(option refname <refname>)
    ++    H: PKT-LINE(option old-oid <old-oid>)
    ++    H: PKT-LINE(option new-oid <new-oid>)
    ++    H: PKT-LINE(option forced-update)
     +    H: ... ...
     +    H: flush-pkt
     +
     +Each command for the 'proc-receive' hook may point to a pseudo-reference
     +and always has a zero-old as its old-oid, while the 'proc-receive' hook
     +may update an alternate reference and the alternate reference may exist
    -+already with a non-zero old-oid.  For this case, this hook may return
    -+different OID and different reference name as extended status of the
    -+report line.
    ++already with a non-zero old-oid.  For this case, this hook will use
    ++"option" directives to report extended attributes for the reference given
    ++by the above "ok/ng" directive.
     +
     +The report of the commands of this hook should have the same order as
     +the input.  The exit status of the 'proc-receive' hook only determines

----

Jiang Xin (7):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  New capability "report-status-v2" for git-push
  receive-pack: feed report options to post-receive
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt              |  14 +
 Documentation/githooks.txt                    |  62 +++
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 459 ++++++++++++++++--
 builtin/send-pack.c                           |  21 +-
 refs.c                                        |  13 +-
 refs.h                                        |   1 +
 remote.c                                      |   7 +-
 remote.h                                      |  16 +-
 send-pack.c                                   |  96 +++-
 t/helper/test-proc-receive.c                  | 175 +++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411-proc-receive-hook.sh                  | 106 ++++
 t/t5411/common-functions.sh                   |  56 +++
 t/t5411/test-0000-standard-git-push.sh        |  84 ++++
 .../test-0001-standard-git-push--porcelain.sh |  86 ++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0011-no-hook-error.sh            |  64 +++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 +++
 t/t5411/test-0013-bad-protocol.sh             | 225 +++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++
 t/t5411/test-0020-report-ng.sh                |  67 +++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 +++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 ++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 ++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 +++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 ++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 ++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 ++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 ++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 169 +++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 172 +++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  79 +++
 .../test-0039-report-mixed-refs--porcelain.sh |  81 ++++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 +++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport-helper.c                            |  53 +-
 transport.c                                   | 121 +++--
 47 files changed, 3737 insertions(+), 93 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v14 1/7] transport: not report a non-head push as a branch
  2020-04-30 15:33                               ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 0/7] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 2/7] receive-pack: add new proc-receive hook Jiang Xin
                                                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh                  | 106 ++++++++++++++++++
 t/t5411/common-functions.sh                   |  56 +++++++++
 t/t5411/test-0000-standard-git-push.sh        |  84 ++++++++++++++
 .../test-0001-standard-git-push--porcelain.sh |  86 ++++++++++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++++++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport.c                                   |   9 +-
 8 files changed, 406 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..67492db940
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,106 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+setup_upstream_and_workbench () {
+	# Refs of upstream : master(A)
+	# Refs of workbench: master(A)  tags/v123
+	test_expect_success "setup upstream and workbench" '
+		rm -rf upstream.git &&
+		rm -rf workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git remote add origin ../upstream.git &&
+			git update-ref refs/heads/master $A &&
+			git tag -m "v123" v123 $A &&
+			git push origin $A:refs/heads/master
+		) &&
+		TAG=$(git -C workbench rev-parse v123) &&
+
+		# setup pre-receive hook
+		write_script upstream.git/hooks/pre-receive <<-\EOF &&
+		exec >&2
+		echo "# pre-receive hook"
+		while read old new ref
+		do
+			echo "pre-receive< $old $new $ref"
+		done
+		EOF
+
+		# setup post-receive hook
+		write_script upstream.git/hooks/post-receive <<-\EOF &&
+		exec >&2
+		echo "# post-receive hook"
+		while read old new ref
+		do
+			echo "post-receive< $old $new $ref"
+		done
+		EOF
+
+		upstream=upstream.git
+	'
+}
+
+run_proc_receive_hook_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	file | local)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Include test cases for both file and HTTP protocol
+	for t in  "$TEST_DIRECTORY"/t5411/test-*.sh
+	do
+		. "$t"
+	done
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run test cases for 'proc-receive' hook on local file protocol.
+run_proc_receive_hook_test local
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup for HTTP protocol" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	set_askpass user@host pass@host &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Run test cases for 'proc-receive' hook on HTTP protocol.
+run_proc_receive_hook_test http
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6580bebd8e
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,56 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
new file mode 100644
index 0000000000..52160aebf7
--- /dev/null
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -0,0 +1,84 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> next
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
new file mode 100644
index 0000000000..4077015b17
--- /dev/null
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -0,0 +1,86 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/next    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	-    :refs/heads/next    [deleted]
+	*    refs/tags/v123:refs/tags/v123    [new tag]
+	*    HEAD:refs/review/master/topic    [new reference]
+	*    HEAD:refs/heads/a/b/c    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
new file mode 100644
index 0000000000..c246f7e68e
--- /dev/null
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -0,0 +1,33 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (pre-receive hook declined)
+	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
new file mode 100644
index 0000000000..510cd3b333
--- /dev/null
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (pre-receive hook declined)
+	!    HEAD:refs/heads/next    [remote rejected] (pre-receive hook declined)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9c6218f568..77938db77f 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 15f5ba4e8f..26fefd13c1 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v14 2/7] receive-pack: add new proc-receive hook
  2020-04-30 15:33                               ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 0/7] New proc-receive hook for centralized workflow Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 1/7] transport: not report a non-head push as a branch Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 3/7] New capability "report-status-v2" for git-push Jiang Xin
                                                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option fall-through)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status can be given in options
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option refname <refname>)
    H: PKT-LINE(option old-oid <old-oid>)
    H: PKT-LINE(option new-oid <new-oid>)
    H: PKT-LINE(option forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in option lines.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" will be
extended in latter commit just like what the "proc-receive" hook reports
to "receive-pack".

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 351 ++++++++++++++++--
 remote.h                                      |  14 +
 t/helper/test-proc-receive.c                  | 175 +++++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411/test-0011-no-hook-error.sh            |  64 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 ++++
 t/t5411/test-0013-bad-protocol.sh             | 225 +++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++++
 t/t5411/test-0020-report-ng.sh                |  67 ++++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 ++++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 +++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 +++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 ++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 +++++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 +++++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 +++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 +++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 159 ++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 162 ++++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  79 ++++
 .../test-0039-report-mixed-refs--porcelain.sh |  81 ++++
 28 files changed, 2645 insertions(+), 32 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh

diff --git a/Makefile b/Makefile
index 3d3a39fc19..607aeabeee 100644
--- a/Makefile
+++ b/Makefile
@@ -722,6 +722,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a00f91c1a0..7575eec320 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
-	const char *error_string;
+	struct ref_push_report report;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -773,7 +776,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	struct command *cmd = state->cmd;
 
 	while (cmd &&
-	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
+	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
@@ -838,6 +841,268 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	int code = 0;
+	int new_options = 1;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *head;
+		const char *refname;
+		char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head);
+			code = -1;
+			continue;
+		}
+		*p++ = '\0';
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
+					new_options = 0;
+				}
+				code = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			else if (!strcmp(key, "fall-through"))
+				/* Fall through, let 'receive-pack' to execute it. */
+				hint->run_proc_receive = 0;
+			continue;
+		}
+
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    head, refname);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!strcmp(head, "ng")) {
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+			code = -1;
+		}
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		new_options = 1;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->report.error_message &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->report.error_message = "proc-receive failed to report status";
+		    code = -1;
+		}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int hook_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					hook_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->report.error_message)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (hook_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1201,7 +1466,7 @@ static void run_update_post_hook(struct command *commands)
 		return;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string || cmd->did_not_exist)
+		if (cmd->report.error_message || cmd->did_not_exist)
 			continue;
 		if (!proc.args.argc)
 			argv_array_push(&proc.args, hook);
@@ -1235,7 +1500,7 @@ static void check_aliased_update_internal(struct command *cmd,
 	if (!dst_name) {
 		rp_error("refusing update to broken symref '%s'", cmd->ref_name);
 		cmd->skip_update = 1;
-		cmd->error_string = "broken symref";
+		cmd->report.error_message = "broken symref";
 		return;
 	}
 	dst_name = strip_namespace(dst_name);
@@ -1262,7 +1527,7 @@ static void check_aliased_update_internal(struct command *cmd,
 		 find_unique_abbrev(&dst_cmd->old_oid, DEFAULT_ABBREV),
 		 find_unique_abbrev(&dst_cmd->new_oid, DEFAULT_ABBREV));
 
-	cmd->error_string = dst_cmd->error_string =
+	cmd->report.error_message = dst_cmd->report.error_message =
 		"inconsistent aliased update";
 }
 
@@ -1291,7 +1556,7 @@ static void check_aliased_updates(struct command *commands)
 	string_list_sort(&ref_list);
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			check_aliased_update(cmd, &ref_list);
 	}
 
@@ -1328,7 +1593,7 @@ static void set_connectivity_errors(struct command *commands,
 				     &opt))
 			continue;
 
-		cmd->error_string = "missing necessary objects";
+		cmd->report.error_message = "missing necessary objects";
 	}
 }
 
@@ -1367,7 +1632,7 @@ static void reject_updates_to_hidden(struct command *commands)
 	prefix_len = refname_full.len;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			continue;
 
 		strbuf_setlen(&refname_full, prefix_len);
@@ -1376,9 +1641,9 @@ static void reject_updates_to_hidden(struct command *commands)
 		if (!ref_is_hidden(cmd->ref_name, refname_full.buf))
 			continue;
 		if (is_null_oid(&cmd->new_oid))
-			cmd->error_string = "deny deleting a hidden ref";
+			cmd->report.error_message = "deny deleting a hidden ref";
 		else
-			cmd->error_string = "deny updating a hidden ref";
+			cmd->report.error_message = "deny updating a hidden ref";
 	}
 
 	strbuf_release(&refname_full);
@@ -1386,7 +1651,7 @@ static void reject_updates_to_hidden(struct command *commands)
 
 static int should_process_cmd(struct command *cmd)
 {
-	return !cmd->error_string && !cmd->skip_update;
+	return !cmd->report.error_message && !cmd->skip_update;
 }
 
 static void warn_if_skipped_connectivity_check(struct command *commands,
@@ -1413,24 +1678,24 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
 		if (!transaction) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "transaction failed to start";
+			cmd->report.error_message = "transaction failed to start";
 			continue;
 		}
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (!cmd->error_string
+		if (!cmd->report.error_message
 		    && ref_transaction_commit(transaction, &err)) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "failed to update ref";
+			cmd->report.error_message = "failed to update ref";
 		}
 		ref_transaction_free(transaction);
 	}
@@ -1453,12 +1718,12 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			goto failure;
 	}
 
@@ -1471,8 +1736,8 @@ static void execute_commands_atomic(struct command *commands,
 
 failure:
 	for (cmd = commands; cmd; cmd = cmd->next)
-		if (!cmd->error_string)
-			cmd->error_string = reported_error;
+		if (!cmd->report.error_message)
+			cmd->report.error_message = reported_error;
 
 cleanup:
 	ref_transaction_free(transaction);
@@ -1489,10 +1754,11 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
-			cmd->error_string = "unpacker error";
+			cmd->report.error_message = "unpacker error";
 		return;
 	}
 
@@ -1518,10 +1784,25 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "pre-receive hook declined";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "pre-receive hook declined";
 		}
 		return;
 	}
@@ -1532,8 +1813,8 @@ static void execute_commands(struct command *commands,
 	 */
 	if (tmp_objdir_migrate(tmp_objdir) < 0) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "unable to migrate objects to permanent storage";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "unable to migrate objects to permanent storage";
 		}
 		return;
 	}
@@ -1544,6 +1825,12 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->report.error_message  && (cmd->run_proc_receive || use_atomic))
+				cmd->report.error_message = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -1905,7 +2192,7 @@ static void update_shallow_info(struct command *commands,
 		if (is_null_oid(&cmd->new_oid))
 			continue;
 		if (ref_status[cmd->index]) {
-			cmd->error_string = "shallow update not allowed";
+			cmd->report.error_message = "shallow update not allowed";
 			cmd->skip_update = 1;
 		}
 	}
@@ -1920,12 +2207,12 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
+						 cmd->ref_name);
 		else
 			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+					 cmd->ref_name, cmd->report.error_message);
 	}
 	packet_buf_flush(&buf);
 
@@ -2029,7 +2316,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		if (!check_cert_push_options(&push_options)) {
 			struct command *cmd;
 			for (cmd = commands; cmd; cmd = cmd->next)
-				cmd->error_string = "inconsistent push options";
+				cmd->report.error_message = "inconsistent push options";
 		}
 
 		prepare_shallow_info(&si, &shallow);
diff --git a/remote.h b/remote.h
index 11d8719b58..2ed2a16e16 100644
--- a/remote.h
+++ b/remote.h
@@ -93,6 +93,20 @@ int for_each_remote(each_remote_fn fn, void *priv);
 
 int remote_has_url(struct remote *remote, const char *url);
 
+struct ref_push_report_options {
+	char *ref_name;
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	unsigned int forced_update:1;
+	struct ref_push_report_options *next;
+};
+
+struct ref_push_report {
+	const char *message;
+	const char *error_message;
+	struct ref_push_report_options *options;
+};
+
 struct ref {
 	struct ref *next;
 	struct object_id old_oid;
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..6ace1863b0
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,175 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int die_version = 0;
+static int die_readline = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static int verbose = 0;
+static int version = 1;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1 || die_version)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ' ||
+		    die_readline)
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_BOOL(0, "die-version", &die_version,
+			 "die during version negotiation"),
+		OPT_BOOL(0, "die-readline", &die_readline,
+			 "die when readline"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 590b2efca7..a0d3966b29 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -46,6 +46,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ddc8e990e9..07034d3f38 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -35,6 +35,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
new file mode 100644
index 0000000000..bb6ec92a92
--- /dev/null
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -0,0 +1,64 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
new file mode 100644
index 0000000000..de3a2cebef
--- /dev/null
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -0,0 +1,66 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
new file mode 100644
index 0000000000..c79983a7b9
--- /dev/null
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -0,0 +1,225 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: bad protocol version: 1
+	remote: error: proc-receive version "0" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: protocol error: expected "old new ref", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
new file mode 100644
index 0000000000..e56a76a5ac
--- /dev/null
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -0,0 +1,160 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
new file mode 100644
index 0000000000..f726b7ca9c
--- /dev/null
+++ b/t/t5411/test-0020-report-ng.sh
@@ -0,0 +1,67 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
new file mode 100644
index 0000000000..2defe93bc9
--- /dev/null
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -0,0 +1,69 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
new file mode 100644
index 0000000000..92a415b929
--- /dev/null
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
new file mode 100644
index 0000000000..d3a5b421ee
--- /dev/null
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -0,0 +1,46 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
new file mode 100644
index 0000000000..c3946f329a
--- /dev/null
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
new file mode 100644
index 0000000000..b2dd4a4173
--- /dev/null
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
new file mode 100644
index 0000000000..d0c4da8b23
--- /dev/null
+++ b/t/t5411/test-0026-push-options.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
new file mode 100644
index 0000000000..c3461232b8
--- /dev/null
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -0,0 +1,82 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push \
+		--porcelain \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
new file mode 100644
index 0000000000..44c99d3831
--- /dev/null
+++ b/t/t5411/test-0030-report-ok.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
new file mode 100644
index 0000000000..58877ef914
--- /dev/null
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -0,0 +1,36 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
new file mode 100644
index 0000000000..a743aa8018
--- /dev/null
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -0,0 +1,256 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
new file mode 100644
index 0000000000..8a50d7efe3
--- /dev/null
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -0,0 +1,265 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/for/a/b/c/topic    [new reference]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
new file mode 100644
index 0000000000..aca2b0676c
--- /dev/null
+++ b/t/t5411/test-0034-report-ft.sh
@@ -0,0 +1,44 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
new file mode 100644
index 0000000000..114a14b544
--- /dev/null
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (fall-through, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    <COMMIT-B>:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
new file mode 100644
index 0000000000..12acf9ea1f
--- /dev/null
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -0,0 +1,159 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
new file mode 100644
index 0000000000..57dc0d17ac
--- /dev/null
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -0,0 +1,162 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
new file mode 100644
index 0000000000..9853e47693
--- /dev/null
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
new file mode 100644
index 0000000000..b3953c749b
--- /dev/null
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -0,0 +1,81 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/bar    [new branch]
+	*    HEAD:refs/heads/baz    [new branch]
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/heads/foo    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v14 3/7] New capability "report-status-v2" for git-push
  2020-04-30 15:33                               ` Jiang Xin
                                                   ` (2 preceding siblings ...)
  2020-05-05 14:41                                 ` [PATCH v14 2/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-05 15:25                                   ` [PATCH v14 8/7] fixup! " Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 4/7] receive-pack: feed report options to post-receive Jiang Xin
                                                   ` (4 subsequent siblings)
  8 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The new introduced "proc-receive" hook may handle a command for a
pseudo-reference with a zero-old as its old-oid, while the hook may
create or update a reference with different name, different new-oid,
and different old-oid (the reference may exist already with a non-zero
old-oid).  Current "report-status" protocol cannot report the status for
such reference rewrite.

Add new capability "report-status-v2" and new report protocol which is
not backward compatible for report of git-push.

If a user pushes to a pseudo-reference "refs/for/master/topic", and
"receive-pack" creates two new references "refs/changes/23/123/1" and
"refs/changes/24/124/1", for client without the knowledge of
"report-status-v2", "receive-pack" will only send "ok/ng" directives in
the report, such as:

    ok ref/for/master/topic

But for client which has the knowledge of "report-status-v2",
"receive-pack" will use "option" directives to report more attributes
for the reference given by the above "ok/ng" directive.

    ok refs/for/master/topic
    option refname refs/changes/23/123/1
    option new-oid <new-oid>
    ok refs/for/master/topic
    option refname refs/changes/24/124/1
    option new-oid <new-oid>

The client will report two new created references to the end user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        |  52 +++++++-
 builtin/send-pack.c                           |  21 +++-
 remote.c                                      |   7 +-
 remote.h                                      |   2 +-
 send-pack.c                                   |  96 +++++++++++---
 t/t5411/test-0032-report-with-options.sh      |  14 +--
 ...est-0033-report-with-options--porcelain.sh |  14 +--
 ...t-0036-report-multi-rewrite-for-one-ref.sh |  11 +-
 ...rt-multi-rewrite-for-one-ref--porcelain.sh |  11 +-
 t/t5411/test-0038-report-mixed-refs.sh        |   2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |   2 +-
 transport-helper.c                            |  53 +++++++-
 transport.c                                   | 118 +++++++++++++-----
 13 files changed, 328 insertions(+), 75 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 7575eec320..8b96a995b8 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -56,6 +56,7 @@ static int advertise_push_options;
 static int unpack_limit = 100;
 static off_t max_input_size;
 static int report_status;
+static int report_status_v2;
 static int use_sideband;
 static int use_atomic;
 static int use_push_options;
@@ -239,7 +240,7 @@ static void show_ref(const char *path, const struct object_id *oid)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status report-status-v2 delete-refs side-band-64k quiet");
 		if (advertise_atomic_push)
 			strbuf_addstr(&cap, " atomic");
 		if (prefer_ofs_delta)
@@ -1912,6 +1913,8 @@ static struct command *read_head_info(struct packet_reader *reader,
 			const char *feature_list = reader->line + linelen + 1;
 			if (parse_feature_request(feature_list, "report-status"))
 				report_status = 1;
+			if (parse_feature_request(feature_list, "report-status-v2"))
+				report_status_v2 = 1;
 			if (parse_feature_request(feature_list, "side-band-64k"))
 				use_sideband = LARGE_PACKET_MAX;
 			if (parse_feature_request(feature_list, "quiet"))
@@ -2223,6 +2226,49 @@ static void report(struct command *commands, const char *unpack_status)
 	strbuf_release(&buf);
 }
 
+static void report_v2(struct command *commands, const char *unpack_status)
+{
+	struct command *cmd;
+	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
+
+	packet_buf_write(&buf, "unpack %s\n",
+			 unpack_status ? unpack_status : "ok");
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		int count = 0;
+
+		if (!cmd->report.error_message)
+			packet_buf_write(&buf, "ok %s\n",
+					 cmd->ref_name);
+		else
+			packet_buf_write(&buf, "ng %s %s\n",
+					 cmd->ref_name,
+					 cmd->report.error_message);
+		for (options = cmd->report.options; options; options = options->next) {
+			if (count++ > 0)
+				packet_buf_write(&buf, "ok %s\n", cmd->ref_name);
+			if (options->ref_name)
+				packet_buf_write(&buf, "option refname %s\n",
+						 options->ref_name);
+			if (options->old_oid)
+				packet_buf_write(&buf, "option old-oid %s\n",
+						 oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				packet_buf_write(&buf, "option new-oid %s\n",
+						 oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				packet_buf_write(&buf, "option forced-update\n");
+		}
+	}
+	packet_buf_flush(&buf);
+
+	if (use_sideband)
+		send_sideband(1, 1, buf.buf, buf.len, use_sideband);
+	else
+		write_or_die(1, buf.buf, buf.len);
+	strbuf_release(&buf);
+}
+
 static int delete_only(struct command *commands)
 {
 	struct command *cmd;
@@ -2331,7 +2377,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
-		if (report_status)
+		if (report_status_v2)
+			report_v2(commands, unpack_status);
+		else if (report_status)
 			report(commands, unpack_status);
 		run_receive_hook(commands, "post-receive", 1,
 				 &push_options);
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index f2c5a34402..4faee72640 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -29,10 +29,12 @@ static struct send_pack_args args;
 static void print_helper_status(struct ref *ref)
 {
 	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
 
 	for (; ref; ref = ref->next) {
 		const char *msg = NULL;
 		const char *res;
+		int count = 0;
 
 		switch(ref->status) {
 		case REF_STATUS_NONE:
@@ -86,14 +88,29 @@ static void print_helper_status(struct ref *ref)
 
 		strbuf_reset(&buf);
 		strbuf_addf(&buf, "%s %s", res, ref->name);
-		if (ref->remote_status)
-			msg = ref->remote_status;
+		if (ref->report.error_message)
+			msg = ref->report.error_message;
 		if (msg) {
 			strbuf_addch(&buf, ' ');
 			quote_two_c_style(&buf, "", msg, 0);
 		}
 		strbuf_addch(&buf, '\n');
 
+		for (options = ref->report.options; options; options = options->next) {
+			if (count++ > 0)
+				strbuf_addf(&buf, "ok %s\n", ref->name);
+			if (options->ref_name)
+				strbuf_addf(&buf, "option refname %s\n",
+						  options->ref_name);
+			if (options->old_oid)
+				strbuf_addf(&buf, "option old-oid %s\n",
+						  oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				strbuf_addf(&buf, "option new-oid %s\n",
+						  oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				strbuf_addf(&buf, "option forced-update\n");
+		}
 		write_or_die(1, buf.buf, buf.len);
 	}
 	strbuf_release(&buf);
diff --git a/remote.c b/remote.c
index 534c6426f1..1f5239650d 100644
--- a/remote.c
+++ b/remote.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "config.h"
+#include "connect.h"
 #include "remote.h"
 #include "refs.h"
 #include "refspec.h"
@@ -789,7 +790,7 @@ struct ref *copy_ref(const struct ref *ref)
 	memcpy(cpy, ref, len);
 	cpy->next = NULL;
 	cpy->symref = xstrdup_or_null(ref->symref);
-	cpy->remote_status = xstrdup_or_null(ref->remote_status);
+	cpy->report.error_message = xstrdup_or_null(ref->report.error_message);
 	cpy->peer_ref = copy_ref(ref->peer_ref);
 	return cpy;
 }
@@ -811,7 +812,7 @@ void free_one_ref(struct ref *ref)
 	if (!ref)
 		return;
 	free_one_ref(ref->peer_ref);
-	free(ref->remote_status);
+	free((void *)ref->report.error_message);
 	free(ref->symref);
 	free(ref);
 }
@@ -2299,4 +2300,4 @@ void apply_push_cas(struct push_cas_option *cas,
 	struct ref *ref;
 	for (ref = remote_refs; ref; ref = ref->next)
 		apply_cas(cas, remote, ref);
-}
+}
\ No newline at end of file
diff --git a/remote.h b/remote.h
index 2ed2a16e16..caa3d5c9ba 100644
--- a/remote.h
+++ b/remote.h
@@ -153,7 +153,7 @@ struct ref {
 		REF_STATUS_EXPECTING_REPORT,
 		REF_STATUS_ATOMIC_PUSH_FAILED
 	} status;
-	char *remote_status;
+	struct ref_push_report report;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
 };
diff --git a/send-pack.c b/send-pack.c
index d1b7edc995..9a9285755c 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -153,24 +153,80 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 {
 	struct ref *hint;
 	int ret;
+	int new_options = 1;
 
 	hint = NULL;
 	ret = receive_unpack_status(reader);
 	while (1) {
+		struct object_id old_oid, new_oid;
+		const char *head;
 		const char *refname;
-		char *msg;
+		char *p;
+
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
-		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
 			error("invalid ref status from remote: %s", reader->line);
 			ret = -1;
 			break;
 		}
+		*p++ = '\0';
+
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					error("'option' without a matching 'ok/ng' directive");
+					new_options = 0;
+				}
+				ret = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			continue;
+		}
 
-		refname = reader->line + 3;
-		msg = strchr(refname, ' ');
-		if (msg)
-			*msg++ = '\0';
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			error("invalid ref status from remote: %s", reader->line);
+			ret = -1;
+			break;
+		}
 
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
@@ -182,19 +238,25 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 					refname);
 			continue;
 		}
-		if (hint->status != REF_STATUS_EXPECTING_REPORT) {
+		if (hint->status != REF_STATUS_EXPECTING_REPORT &&
+		    hint->status != REF_STATUS_OK &&
+		    hint->status != REF_STATUS_REMOTE_REJECT) {
 			warning("remote reported status on unexpected ref: %s",
 					refname);
 			continue;
 		}
-
-		if (reader->line[0] == 'o' && reader->line[1] == 'k')
-			hint->status = REF_STATUS_OK;
-		else
+		if (!strcmp(head, "ng")) {
 			hint->status = REF_STATUS_REMOTE_REJECT;
-		hint->remote_status = xstrdup_or_null(msg);
-		/* start our next search from the next ref */
-		hint = hint->next;
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+		} else {
+			hint->status = REF_STATUS_OK;
+			if (p)
+				hint->report.message = xstrdup(p);
+		}
+		new_options = 1;
 	}
 	return ret;
 }
@@ -371,6 +433,8 @@ int send_pack(struct send_pack_args *args,
 	/* Does the other end support the reporting? */
 	if (server_supports("report-status"))
 		status_report = 1;
+	if (server_supports("report-status-v2"))
+		status_report = 2;
 	if (server_supports("delete-refs"))
 		allow_deleting_refs = 1;
 	if (server_supports("ofs-delta"))
@@ -418,8 +482,10 @@ int send_pack(struct send_pack_args *args,
 
 	use_push_options = push_options_supported && args->push_options;
 
-	if (status_report)
+	if (status_report == 1)
 		strbuf_addstr(&cap_buf, " report-status");
+	else if (status_report == 2)
+		strbuf_addstr(&cap_buf, " report-status-v2");
 	if (use_sideband)
 		strbuf_addstr(&cap_buf, " side-band-64k");
 	if (quiet_supported && (args->quiet || !args->progress))
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a743aa8018..3742ed3c3d 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -56,7 +56,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -89,7 +89,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -123,7 +123,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -155,7 +155,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -189,7 +189,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -241,9 +241,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 8a50d7efe3..929c46ccd8 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -57,7 +57,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -92,7 +92,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -127,7 +127,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -160,7 +160,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -195,7 +195,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual
@@ -249,9 +249,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
-	*    HEAD:refs/for/master/topic    [new reference]
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 12acf9ea1f..64662afdc6 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -147,7 +151,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index 57dc0d17ac..a371c8d610 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/23/123/1    [new reference]
+	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 9853e47693..9b060b2f84 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -52,7 +52,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index b3953c749b..537d4046b9 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -52,7 +52,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	*    HEAD:refs/heads/baz    [new branch]
 	*    HEAD:refs/for/next/topic    [new reference]
 	*    HEAD:refs/heads/foo    [new branch]
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/transport-helper.c b/transport-helper.c
index a46afcb69d..21d6e47966 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -725,6 +725,49 @@ static int push_update_ref_status(struct strbuf *buf,
 {
 	char *refname, *msg;
 	int status, forced = 0;
+	static struct ref *hint = NULL;
+	static int new_options = 1;
+
+	if (starts_with(buf->buf, "option ")) {
+		struct ref_push_report_options *options;
+		struct object_id old_oid, new_oid;
+		const char *key, *val;
+		char *p;
+
+		if (!hint)
+			die(_("'option' without a matching 'ok/error' directive"));
+		options = hint->report.options;
+		while (options && options->next)
+			options = options->next;
+		if (new_options) {
+			if (!options) {
+				hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = hint->report.options;
+			} else {
+				options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = options->next;
+			}
+			new_options = 0;
+		}
+		assert(options);
+		key = buf->buf + 7;
+		p = strchr(key, ' ');
+		if (p)
+			*p++ = '\0';
+		val = p;
+		if (!strcmp(key, "refname"))
+			options->ref_name = xstrdup_or_null(val);
+		else if (!strcmp(key, "old-oid") && val &&
+			 !parse_oid_hex(val, &old_oid, &val))
+			options->old_oid = oiddup(&old_oid);
+		else if (!strcmp(key, "new-oid") && val &&
+			 !parse_oid_hex(val, &new_oid, &val))
+			options->new_oid = oiddup(&new_oid);
+		else if (!strcmp(key, "forced-update"))
+			options->forced_update = 1;
+		/* Not update remote namespace again. */
+		return 1;
+	}
 
 	if (starts_with(buf->buf, "ok ")) {
 		status = REF_STATUS_OK;
@@ -787,8 +830,11 @@ static int push_update_ref_status(struct strbuf *buf,
 		*ref = find_ref_by_name(remote_refs, refname);
 	if (!*ref) {
 		warning(_("helper reported unexpected status of %s"), refname);
+		hint = NULL;
 		return 1;
 	}
+	hint = *ref;
+	new_options = 1;
 
 	if ((*ref)->status != REF_STATUS_NONE) {
 		/*
@@ -801,7 +847,12 @@ static int push_update_ref_status(struct strbuf *buf,
 
 	(*ref)->status = status;
 	(*ref)->forced_update |= forced;
-	(*ref)->remote_status = msg;
+	if (msg) {
+		if (status == REF_STATUS_OK)
+			(*ref)->report.message = msg;
+		else
+			(*ref)->report.error_message = msg;
+	}
 	return !(status == REF_STATUS_OK);
 }
 
diff --git a/transport.c b/transport.c
index 26fefd13c1..2e2b7de036 100644
--- a/transport.c
+++ b/transport.c
@@ -461,13 +461,21 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
+			     struct ref_push_report_options *options,
 			     int porcelain, int summary_width)
 {
+	char *to_name;
+
+	if (options && options->ref_name)
+		to_name = options->ref_name;
+	else
+		to_name = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +489,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -493,27 +503,52 @@ static void print_ref_status(char flag, const char *summary,
 	}
 }
 
-static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
+static void print_ok_ref_status(struct ref *ref,
+				struct ref_push_report_options *options,
+				int porcelain, int summary_width)
 {
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	const char *ref_name;
+	int forced_update;
+
+	if (options && options->old_oid)
+		old_oid = options->old_oid;
+	else
+		old_oid = &ref->old_oid;
+	if (options && options->new_oid)
+		new_oid = options->new_oid;
+	else
+		new_oid = &ref->new_oid;
+	if (options && options->forced_update)
+		forced_update = options->forced_update;
+	else
+		forced_update = ref->forced_update;
+	if (options && options->ref_name)
+		ref_name = options->ref_name;
+	else
+		ref_name = ref->name;
+
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
-				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+				 options, porcelain, summary_width);
+	else if (is_null_oid(old_oid))
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(ref_name, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(ref_name, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 ref, ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
 
-		strbuf_add_unique_abbrev(&quickref, &ref->old_oid,
+		strbuf_add_unique_abbrev(&quickref, old_oid,
 					 DEFAULT_ABBREV);
-		if (ref->forced_update) {
+		if (forced_update) {
 			strbuf_addstr(&quickref, "...");
 			type = '+';
 			msg = "forced update";
@@ -522,17 +557,18 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 			type = ' ';
 			msg = NULL;
 		}
-		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
+		strbuf_add_unique_abbrev(&quickref, new_oid,
 					 DEFAULT_ABBREV);
 
 		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
-				 int porcelain, int summary_width)
+static int _print_one_push_status(struct ref *ref, const char *dest, int count,
+				  struct ref_push_report_options *options,
+				  int porcelain, int summary_width)
 {
 	if (!count) {
 		char *url = transport_anonymize_url(dest);
@@ -543,65 +579,89 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	switch(ref->status) {
 	case REF_STATUS_NONE:
 		print_ref_status('X', "[no match]", ref, NULL, NULL,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
 		print_ref_status('!', "[rejected]", ref, NULL,
 				 "remote does not support deleting refs",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
 		print_ref_status('=', "[up to date]", ref,
-				 ref->peer_ref, NULL, porcelain, summary_width);
+				 ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "non-fast-forward", porcelain, summary_width);
+				 "non-fast-forward",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "already exists", porcelain, summary_width);
+				 "already exists",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "fetch first", porcelain, summary_width);
+				 "fetch first",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "needs force", porcelain, summary_width);
+				 "needs force",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "stale info", porcelain, summary_width);
+				 "stale info",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
 				 "new shallow roots not allowed",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
 		print_ref_status('!', "[remote rejected]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
-				 ref->remote_status, porcelain, summary_width);
+				 ref->report.error_message,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
 		print_ref_status('!', "[remote failure]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "atomic push failed", porcelain, summary_width);
+				 "atomic push failed",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-		print_ok_ref_status(ref, porcelain, summary_width);
+		print_ok_ref_status(ref, options, porcelain, summary_width);
 		break;
 	}
 
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	struct ref_push_report_options *options;
+	int n = 0;
+
+	if (!ref->report.options)
+		return _print_one_push_status(ref, dest, count,
+					      NULL, porcelain, summary_width);
+
+	for (options = ref->report.options; options; options = options->next)
+		_print_one_push_status(ref, dest, count + n++,
+				       options, porcelain, summary_width);
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v14 4/7] receive-pack: feed report options to post-receive
  2020-04-30 15:33                               ` Jiang Xin
                                                   ` (3 preceding siblings ...)
  2020-05-05 14:41                                 ` [PATCH v14 3/7] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                                                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When commands are fed to the "post-receive" hook, report options will
be parsed and the real old-oid, new-oid, reference name will feed to
the "post-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        | 29 ++++++++++++++++---
 t/t5411/test-0032-report-with-options.sh      | 14 ++++-----
 ...est-0033-report-with-options--porcelain.sh | 14 ++++-----
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 11 +++++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 11 +++++--
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 7 files changed, 57 insertions(+), 26 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 8b96a995b8..f57d997121 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -775,17 +775,38 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 {
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
+	static struct ref_push_report_options *options = NULL;
 
 	while (cmd &&
 	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
+	if (!bufp)
+		return 0; /* OK, can feed something. */
 	strbuf_reset(&state->buf);
-	strbuf_addf(&state->buf, "%s %s %s\n",
-		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
-		    cmd->ref_name);
-	state->cmd = cmd->next;
+	if (!options)
+		options = cmd->report.options;
+	if (options) {
+		struct object_id *old_oid;
+		struct object_id *new_oid;
+		char *ref_name;
+
+		old_oid = options->old_oid ? options->old_oid : &cmd->old_oid;
+		new_oid = options->new_oid ? options->new_oid : &cmd->new_oid;
+		ref_name = options->ref_name ? options->ref_name : cmd->ref_name;
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(old_oid), oid_to_hex(new_oid),
+			    ref_name);
+		options = options->next;
+		if (!options)
+			state->cmd = cmd->next;
+	} else {
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
+			    cmd->ref_name);
+		state->cmd = cmd->next;
+	}
 	if (bufp) {
 		*bufp = state->buf.buf;
 		*sizep = state->buf.len;
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index 3742ed3c3d..b77b78c49f 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -54,7 +54,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -87,7 +87,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -121,7 +121,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
@@ -153,7 +153,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
@@ -187,7 +187,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
@@ -237,9 +237,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 929c46ccd8..b817d645f1 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -55,7 +55,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -90,7 +90,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -125,7 +125,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
@@ -158,7 +158,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
@@ -193,7 +193,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
@@ -245,9 +245,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 64662afdc6..27d58edd14 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 * [new reference] HEAD -> refs/changes/24/124/1
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/24/124/1
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/23/123/1
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index a371c8d610..2519c1acd8 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	*    HEAD:refs/changes/24/124/1    [new reference]
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/24/124/1    [new reference]
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
@@ -151,7 +155,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/23/123/1    [new reference]
 	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 9b060b2f84..b3fcd2283e 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -45,7 +45,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 537d4046b9..693b55549f 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -45,7 +45,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
 	*    HEAD:refs/heads/bar    [new branch]
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v14 5/7] refs.c: refactor to reuse ref_is_hidden()
  2020-04-30 15:33                               ` Jiang Xin
                                                   ` (4 preceding siblings ...)
  2020-05-05 14:41                                 ` [PATCH v14 4/7] receive-pack: feed report options to post-receive Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
                                                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 13 +++++++++----
 refs.h |  1 +
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 224ff66c7b..76a91ac0f6 100644
--- a/refs.c
+++ b/refs.c
@@ -1344,14 +1344,14 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 	return 0;
 }
 
-int ref_is_hidden(const char *refname, const char *refname_full)
+int ref_matches(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
@@ -1377,6 +1377,11 @@ int ref_is_hidden(const char *refname, const char *refname_full)
 	return 0;
 }
 
+int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_matches(hide_refs, refname, refname_full);
+}
+
 const char *find_descendant_ref(const char *dirname,
 				const struct string_list *extras,
 				const struct string_list *skip)
diff --git a/refs.h b/refs.h
index a92d2c74c8..9907dd12b0 100644
--- a/refs.h
+++ b/refs.h
@@ -727,6 +727,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_matches(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v14 6/7] receive-pack: new config receive.procReceiveRefs
  2020-04-30 15:33                               ` Jiang Xin
                                                   ` (5 preceding siblings ...)
  2020-05-05 14:41                                 ` [PATCH v14 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-05 14:41                                 ` [PATCH v14 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  2020-05-07 17:27                                 ` [PATCH v12 3/7] receive-pack: add new " Jeff King
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings match the reference names of the commands
which are sent from git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt              |  14 +++
 builtin/receive-pack.c                        |  43 +++++--
 t/t5411/test-0010-proc-receive-settings.sh    |   7 ++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++++++++++++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 ++++++++++++++++++
 5 files changed, 284 insertions(+), 7 deletions(-)
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..e7b967feeb 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running the hook "proc-receive".
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index f57d997121..88926c8664 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -77,6 +77,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -229,6 +230,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_append(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1810,15 +1825,26 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2317,6 +2343,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2434,5 +2462,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/test-0010-proc-receive-settings.sh b/t/t5411/test-0010-proc-receive-settings.sh
new file mode 100644
index 0000000000..a36809927b
--- /dev/null
+++ b/t/t5411/test-0010-proc-receive-settings.sh
@@ -0,0 +1,7 @@
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
new file mode 100644
index 0000000000..b07c999f53
--- /dev/null
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -0,0 +1,113 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
new file mode 100644
index 0000000000..7092db0a06
--- /dev/null
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -0,0 +1,114 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+	-    :refs/heads/foo    [deleted]
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v14 7/7] doc: add documentation for the proc-receive hook
  2020-04-30 15:33                               ` Jiang Xin
                                                   ` (6 preceding siblings ...)
  2020-05-05 14:41                                 ` [PATCH v14 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-05-05 14:41                                 ` Jiang Xin
  2020-05-07 17:27                                 ` [PATCH v12 3/7] receive-pack: add new " Jeff King
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 14:41 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 62 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 81f2a87e88..d66154f606 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,68 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option fall-through)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status can be given in options
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option refname <refname>)
+    H: PKT-LINE(option old-oid <old-oid>)
+    H: PKT-LINE(option new-oid <new-oid>)
+    H: PKT-LINE(option forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook will use
+"option" directives to report extended attributes for the reference given
+by the above "ok/ng" directive.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v14 8/7] fixup! New capability "report-status-v2" for git-push
  2020-05-05 14:41                                 ` [PATCH v14 3/7] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-05-05 15:25                                   ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-05 15:25 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Fixup commit for patch v14 3/7.  Will queue this fix for reroll v15 with others.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/send-pack.c | 2 +-
 send-pack.c         | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 4faee72640..4fff1c62d3 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -109,7 +109,7 @@ static void print_helper_status(struct ref *ref)
 				strbuf_addf(&buf, "option new-oid %s\n",
 						  oid_to_hex(options->new_oid));
 			if (options->forced_update)
-				strbuf_addf(&buf, "option forced-update\n");
+				strbuf_addstr(&buf, "option forced-update\n");
 		}
 		write_or_die(1, buf.buf, buf.len);
 	}
diff --git a/send-pack.c b/send-pack.c
index 9a9285755c..82b9760340 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -253,8 +253,7 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 				hint->report.error_message = "failed";
 		} else {
 			hint->status = REF_STATUS_OK;
-			if (p)
-				hint->report.message = xstrdup(p);
+			hint->report.message = xstrdup_or_null(p);
 		}
 		new_options = 1;
 	}
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v14 0/7] New proc-receive hook for centralized workflow
  2020-05-05 14:41                                 ` [PATCH v14 0/7] New proc-receive hook for centralized workflow Jiang Xin
@ 2020-05-06 23:14                                   ` Junio C Hamano
  2020-05-07  1:37                                     ` Jiang Xin
                                                       ` (8 more replies)
  0 siblings, 9 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-05-06 23:14 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Jeff King, Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
>
> This topic introduces a new hook named "proc-receive" for
> "receive-pack".  We can use this hook to implement a Gerrit-like
> centralized workflow.

https://github.com/git/git/runs/651179641 shows us that osx-clang
job ends like so:

Thanks.


Test Summary Report
-------------------
t5411-proc-receive-hook.sh                       (Wstat: 256 Tests: 273 Failed: 0)
  Non-zero exit status: 1
  Parse errors: Tests out of sequence.  Found (27) but expected (26)
                Tests out of sequence.  Found (28) but expected (27)
                Tests out of sequence.  Found (29) but expected (28)
                Tests out of sequence.  Found (30) but expected (29)
                Tests out of sequence.  Found (31) but expected (30)
Displayed the first 5 of 249 TAP syntax errors.
Re-run prove with the -p option to see them all.
Files=906, Tests=22778, 1347 wallclock secs ( 8.21 usr  3.04 sys + 1322.42 cusr 2031.60 csys = 3365.27 CPU)
Result: FAIL
make[1]: *** [prove] Error 1
make: *** [test] Error 2
##[error]Process completed with exit code 2.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v14 0/7] New proc-receive hook for centralized workflow
  2020-05-06 23:14                                   ` Junio C Hamano
@ 2020-05-07  1:37                                     ` Jiang Xin
  2020-05-07 11:18                                       ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                       ` (7 subsequent siblings)
  8 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-05-07  1:37 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Jeff King, Git List, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年5月7日周四 上午7:14写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> >
> > This topic introduces a new hook named "proc-receive" for
> > "receive-pack".  We can use this hook to implement a Gerrit-like
> > centralized workflow.
>
> https://github.com/git/git/runs/651179641 shows us that osx-clang
> job ends like so:
>
> Thanks.
>
>
> Test Summary Report
> -------------------
> t5411-proc-receive-hook.sh                       (Wstat: 256 Tests: 273 Failed: 0)
>   Non-zero exit status: 1
>   Parse errors: Tests out of sequence.  Found (27) but expected (26)
>                 Tests out of sequence.  Found (28) but expected (27)
>                 Tests out of sequence.  Found (29) but expected (28)
>                 Tests out of sequence.  Found (30) but expected (29)
>                 Tests out of sequence.  Found (31) but expected (30)

It seems that the status report for git-push ("ok <refname>") confuses
the test. Will fix it in next reroll.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v14 0/7] New proc-receive hook for centralized workflow
  2020-05-07  1:37                                     ` Jiang Xin
@ 2020-05-07 11:18                                       ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 11:18 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Jeff King, Git List, Jiang Xin, Derrick Stolee

Jiang Xin <worldhello.net@gmail.com> 于2020年5月7日周四 上午9:37写道:
>
> Junio C Hamano <gitster@pobox.com> 于2020年5月7日周四 上午7:14写道:
> >
> > Jiang Xin <worldhello.net@gmail.com> writes:
> >
> > > From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> > >
> > > This topic introduces a new hook named "proc-receive" for
> > > "receive-pack".  We can use this hook to implement a Gerrit-like
> > > centralized workflow.
> >
> > https://github.com/git/git/runs/651179641 shows us that osx-clang
> > job ends like so:
> >
> > Thanks.
> >
> >
> > Test Summary Report
> > -------------------
> > t5411-proc-receive-hook.sh                       (Wstat: 256 Tests: 273 Failed: 0)
> >   Non-zero exit status: 1
> >   Parse errors: Tests out of sequence.  Found (27) but expected (26)
> >                 Tests out of sequence.  Found (28) but expected (27)
> >                 Tests out of sequence.  Found (29) but expected (28)
> >                 Tests out of sequence.  Found (30) but expected (29)
> >                 Tests out of sequence.  Found (31) but expected (30)
>
> It seems that the status report for git-push ("ok <refname>") confuses
> the test. Will fix it in next reroll.

This morning I cannot find the specific error from the build log, but
now I find the error output inside the job tagged as "Run
ci/print-test-failures.sh":

    ++++ diff -u expect actual
    --- expect 2020-05-06 22:47:02.000000000 +0000
    +++ actual 2020-05-06 22:47:02.000000000 +0000
    @@ -2,5 +2,6 @@
    remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    remote: # proc-receive hook
    remote: fatal: protocol error: expected "old new ref", got
"<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    -To <URL/of/upstream.git>
    - ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive
failed to report status)
    +fatal: unable to write flush packet: Broken pipe
    +fatal: the remote end hung up unexpectedly
    +fatal: the remote end hung up unexpectedly

In order to increase the code coverage (thanks Stolee for sending a
nice code coverage report), I add an option "--die-readline" for
test-helper (t/helper/test-proc-receive):

                if (parse_oid_hex(reader->line, &old_oid, &p) ||
                    *p++ != ' ' ||
                    parse_oid_hex(p, &new_oid, &p) ||
                    *p++ != ' ' ||
                    die_readline)
                        die("protocol error: expected 'old new ref', got '%s'",
                            reader->line);

Hook "proc-receive" hook will die early with the "--die_readline"
option, and "receive-pack" should continue running because
"receive-pack" ignores the SIG_PIPE signal, but I don't know why the
behaviors of "receive-pack" are quite different on different
platforms.

I will rewrite the test case using "grep" instead of "test_cmp", and
may send another reroll tonight.

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v15 0/7] New proc-receive hook for centralized workflow
  2020-05-06 23:14                                   ` Junio C Hamano
  2020-05-07  1:37                                     ` Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                         ` (11 more replies)
  2020-05-07 16:10                                     ` [PATCH v15 1/7] transport: not report a non-head push as a branch Jiang Xin
                                                       ` (6 subsequent siblings)
  8 siblings, 12 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This topic introduces a new hook named "proc-receive" for
"receive-pack".  We can use this hook to implement a Gerrit-like
centralized workflow.

## Changes since v14

1. Fixed broken test case in patch 2/7, and all test cases passed.
   See: https://github.com/jiangxin/git/runs/653492851

2. Bugfix: if "proc-receive" report a "ng" status, other successfully
   updated references will be reported as failure.  Fixed in patch 2/7.

3. Add test case for report status v1 in "t5411/once-0010" in patch 3/7.

## Range-diff v14...v15

1:  805db643dd ! 1:  06dd984d35 transport: not report a non-head push as a branch
    @@ t/t5411-proc-receive-hook.sh (new)
     +			git remote add origin ../upstream.git &&
     +			git update-ref refs/heads/master $A &&
     +			git tag -m "v123" v123 $A &&
    -+			git push origin $A:refs/heads/master
    ++			git push origin $B:refs/heads/master &&
    ++			git -C ../upstream.git update-ref \
    ++				refs/heads/master $A
     +		) &&
     +		TAG=$(git -C workbench rev-parse v123) &&
     +
    @@ t/t5411-proc-receive-hook.sh (new)
     +		PROTOCOL="HTTP protocol"  +		URL_PREFIX="http://.*"
     +		;;
    -+	file | local)
    ++	local)
     +		PROTOCOL="builtin protocol"
     +		URL_PREFIX="\.\."
     +		;;
2:  d6db85a4e5 ! 2:  1c11a5dcc6 receive-pack: add new proc-receive hook
    @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
     +	if (run_proc_receive &&
     +	    run_proc_receive_hook(commands, push_options))
     +		for (cmd = commands; cmd; cmd = cmd->next)
    -+			if (!cmd->report.error_message  && (cmd->run_proc_receive || use_atomic))
    ++			if (!cmd->report.error_message &&
    ++			    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED) &&
    ++			    (cmd->run_proc_receive || use_atomic))
     +				cmd->report.error_message = "fail to run proc-receive hook";
     +
      	if (use_atomic)
    @@ t/t5411/test-0013-bad-protocol.sh (new)
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +
    -+	cat >expect <<-EOF &&
    -+	remote: # pre-receive hook
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: # proc-receive hook
    -+	remote: fatal: protocol error: expected "old new ref", got "<ZERO-OID> <COMMIT-A> refs/for/master/topic"
    -+	To <URL/of/upstream.git>
    -+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
    -+	EOF
    -+	test_cmp expect actual &&
    ++	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
     +
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
    @@ t/t5411/test-0038-report-mixed-refs.sh (new)
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ok refs/for/next/topic" \
    ++		-r "ok refs/for/next/topic2" \
    ++		-r "ng refs/for/next/topic1 fail to call Web API" \
     +		-r "ok refs/for/master/topic" \
     +		-r "option refname refs/for/master/topic" \
     +		-r "option old-oid $A" \
    @@ t/t5411/test-0038-report-mixed-refs.sh (new)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
     +test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
    -+	git -C workbench push origin \
    ++	test_must_fail git -C workbench push origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/heads/bar \
     +		HEAD:refs/heads/baz \
    -+		HEAD:refs/for/next/topic \
    ++		HEAD:refs/for/next/topic2 \
    ++		HEAD:refs/for/next/topic1 \
     +		HEAD:refs/heads/foo \
     +		HEAD:refs/for/master/topic \
    ++		HEAD:refs/for/next/topic3 \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0038-report-mixed-refs.sh (new)
     +	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
     +	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
    ++	remote: proc-receive> ok refs/for/next/topic2
    ++	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
     +	remote: proc-receive> ok refs/for/master/topic
     +	remote: proc-receive> option refname refs/for/master/topic
     +	remote: proc-receive> option old-oid <COMMIT-A>
    @@ t/t5411/test-0038-report-mixed-refs.sh (new)
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	 <OID-A>..<OID-B> <COMMIT-B> -> master
     +	 * [new branch] HEAD -> bar
     +	 * [new branch] HEAD -> baz
    -+	 * [new reference] HEAD -> refs/for/next/topic
    ++	 * [new reference] HEAD -> refs/for/next/topic2
     +	 * [new branch] HEAD -> foo
     +	 * [new reference] HEAD -> refs/for/master/topic
    ++	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
    ++	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
     +	EOF
     +	test_cmp expect actual &&
     +	git -C "$upstream" show-ref >out &&
    @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh (new)
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v \
    -+		-r "ok refs/for/next/topic" \
    ++		-r "ok refs/for/next/topic2" \
    ++		-r "ng refs/for/next/topic1 fail to call Web API" \
     +		-r "ok refs/for/master/topic" \
     +		-r "option refname refs/for/master/topic" \
     +		-r "option old-oid $A" \
    @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh (new)
     +# Refs of workbench: master(A)  tags/v123
     +# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
     +test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
    -+	git -C workbench push --porcelain origin \
    ++	test_must_fail git -C workbench push --porcelain origin \
     +		$B:refs/heads/master \
     +		HEAD:refs/heads/bar \
     +		HEAD:refs/heads/baz \
    -+		HEAD:refs/for/next/topic \
    ++		HEAD:refs/for/next/topic2 \
    ++		HEAD:refs/for/next/topic1 \
     +		HEAD:refs/heads/foo \
     +		HEAD:refs/for/master/topic \
    ++		HEAD:refs/for/next/topic3 \
     +		>out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh (new)
     +	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    -+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
     +	remote: # proc-receive hook
    -+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
     +	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
    -+	remote: proc-receive> ok refs/for/next/topic
    ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
    ++	remote: proc-receive> ok refs/for/next/topic2
    ++	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
     +	remote: proc-receive> ok refs/for/master/topic
     +	remote: proc-receive> option refname refs/for/master/topic
     +	remote: proc-receive> option old-oid <COMMIT-A>
    @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh (new)
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    -+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	To <URL/of/upstream.git>
     +	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
     +	*    HEAD:refs/heads/bar    [new branch]
     +	*    HEAD:refs/heads/baz    [new branch]
    -+	*    HEAD:refs/for/next/topic    [new reference]
    ++	*    HEAD:refs/for/next/topic2    [new reference]
     +	*    HEAD:refs/heads/foo    [new branch]
     +	*    HEAD:refs/for/master/topic    [new reference]
    ++	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
    ++	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
     +	Done
     +	EOF
     +	test_cmp expect actual &&
3:  342254d81d ! 3:  c7f731b9f6 New capability "report-status-v2" for git-push
    @@ remote.c: void free_one_ref(struct ref *ref)
      	free(ref->symref);
      	free(ref);
      }
    -@@ remote.c: void apply_push_cas(struct push_cas_option *cas,
    - 	struct ref *ref;
    - 	for (ref = remote_refs; ref; ref = ref->next)
    - 		apply_cas(cas, remote, ref);
    --}
    -+}
    - \ No newline at end of file
     
      ## remote.h ##
     @@ remote.h: struct ref {
    @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref
      		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
      			break;
     -		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
    +-			error("invalid ref status from remote: %s", reader->line);
     +		head = reader->line;
     +		p = strchr(head, ' ');
     +		if (!p) {
    - 			error("invalid ref status from remote: %s", reader->line);
    ++			error("invalid status line from remote: %s", reader->line);
      			ret = -1;
      			break;
      		}
    @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref
     -		msg = strchr(refname, ' ');
     -		if (msg)
     -			*msg++ = '\0';
    -+		refname = p;
    -+		p = strchr(refname, ' ');
    -+		if (p)
    -+			*p++ = '\0';
    +-
     +		if (strcmp(head, "ok") && strcmp(head, "ng")) {
    -+			error("invalid ref status from remote: %s", reader->line);
    ++			error("invalid ref status from remote: %s", head);
     +			ret = -1;
     +			break;
     +		}
    - 
    ++		refname = p;
    ++		p = strchr(refname, ' ');
    ++		if (p)
    ++			*p++ = '\0';
      		/* first try searching at our hint, falling back to all refs */
      		if (hint)
    + 			hint = find_ref_by_name(hint, refname);
     @@ send-pack.c: static int receive_status(struct packet_reader *reader, struct ref *refs)
      					refname);
      			continue;
    @@ send-pack.c: int send_pack(struct send_pack_args *args,
      		strbuf_addstr(&cap_buf, " side-band-64k");
      	if (quiet_supported && (args->quiet || !args->progress))
     
    + ## t/t5411-proc-receive-hook.sh ##
    +@@ t/t5411-proc-receive-hook.sh: run_proc_receive_hook_test() {
    + # Initialize the upstream repository and local workbench.
    + setup_upstream_and_workbench
    + 
    ++# Load test cases that only need to be executed once.
    ++for t in  "$TEST_DIRECTORY"/t5411/once-*.sh
    ++do
    ++	. "$t"
    ++done
    ++
    + # Run test cases for 'proc-receive' hook on local file protocol.
    + run_proc_receive_hook_test local
    + 
    +
    + ## t/t5411/once-0010-report-status-v1.sh (new) ##
    +@@
    ++test_expect_success "setup proc-receive hook" '
    ++	write_script "$upstream/hooks/proc-receive" <<-EOF
    ++	printf >&2 "# proc-receive hook\n"
    ++	test-tool proc-receive -v \
    ++		-r "ok refs/for/master/topic1" \
    ++		-r "option fall-through" \
    ++		-r "ok refs/for/master/topic2" \
    ++		-r "option refname refs/for/changes/23/123/1" \
    ++		-r "option new-oid $A" \
    ++		-r "ok refs/for/master/topic2" \
    ++		-r "option refname refs/for/changes/24/124/2" \
    ++		-r "option old-oid $B" \
    ++		-r "option new-oid $A" \
    ++		-r "option forced-update" \
    ++		-r "ng refs/for/next/topic target branch not exist"
    ++	EOF
    ++'
    ++
    ++# Refs of upstream : master(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++# git push         : (B)                   refs/for/master/topic1(A)  foo(A)  refs/for/next/topic(A)  refs/for/master/topic2(A)
    ++test_expect_success "proc-receive: report status v1" '
    ++	{
    ++		printf "%s %s refs/heads/master\0report-status\n" \
    ++			$A $B | packetize &&
    ++		printf "%s %s refs/for/master/topic1\n" \
    ++			$ZERO_OID $A | packetize &&
    ++		printf "%s %s refs/heads/foo\n" \
    ++			$ZERO_OID $A | packetize &&
    ++		printf "%s %s refs/for/next/topic\n" \
    ++			$ZERO_OID $A | packetize &&
    ++		printf "%s %s refs/for/master/topic2\n" \
    ++			$ZERO_OID $A | packetize &&
    ++		printf 0000 &&
    ++		printf "" | git -C "$upstream" pack-objects --stdout
    ++	} | git receive-pack "$upstream" --stateless-rpc \
    ++	>out 2>&1 &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	# pre-receive hook
    ++	pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
    ++	pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
    ++	# proc-receive hook
    ++	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
    ++	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    ++	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
    ++	proc-receive> ok refs/for/master/topic1
    ++	proc-receive> option fall-through
    ++	proc-receive> ok refs/for/master/topic2
    ++	proc-receive> option refname refs/for/changes/23/123/1
    ++	proc-receive> option new-oid <COMMIT-A>
    ++	proc-receive> ok refs/for/master/topic2
    ++	proc-receive> option refname refs/for/changes/24/124/2
    ++	proc-receive> option old-oid <COMMIT-B>
    ++	proc-receive> option new-oid <COMMIT-A>
    ++	proc-receive> option forced-update
    ++	proc-receive> ng refs/for/next/topic target branch not exist
    ++	000eunpack ok
    ++	0019ok refs/heads/master
    ++	001eok refs/for/master/topic1
    ++	0016ok refs/heads/foo
    ++	0033ng refs/for/next/topic target branch not exist
    ++	001eok refs/for/master/topic2
    ++	0000# post-receive hook
    ++	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    ++	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
    ++	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    ++	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
    ++	EOF
    ++	test_cmp expect actual &&
    ++
    ++	git -C "$upstream" show-ref >out &&
    ++	make_user_friendly_and_stable_output <out >actual &&
    ++	cat >expect <<-EOF &&
    ++	<COMMIT-A> refs/for/master/topic1
    ++	<COMMIT-A> refs/heads/foo
    ++	<COMMIT-B> refs/heads/master
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++# Refs of upstream : master(B)             foo(A)  refs/for/master/topic1(A)
    ++# Refs of workbench: master(A)  tags/v123
    ++test_expect_success "cleanup" '
    ++	(
    ++		cd "$upstream" &&
    ++		rm -f "hooks/proc-receive" &&
    ++		git update-ref refs/heads/master $A &&
    ++		git update-ref -d refs/for/master/topic1 &&
    ++		git update-ref -d refs/heads/foo
    ++	)
    ++'
    +
      ## t/t5411/test-0032-report-with-options.sh ##
     @@ t/t5411/test-0032-report-with-options.sh: test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
      	remote: # post-receive hook
    @@ t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh: test_expect_su
      ## t/t5411/test-0038-report-mixed-refs.sh ##
     @@ t/t5411/test-0038-report-mixed-refs.sh: test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
      	 * [new branch] HEAD -> baz
    - 	 * [new reference] HEAD -> refs/for/next/topic
    + 	 * [new reference] HEAD -> refs/for/next/topic2
      	 * [new branch] HEAD -> foo
     -	 * [new reference] HEAD -> refs/for/master/topic
     +	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
    + 	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
    + 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
      	EOF
    - 	test_cmp expect actual &&
    - 	git -C "$upstream" show-ref >out &&
     
      ## t/t5411/test-0039-report-mixed-refs--porcelain.sh ##
     @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh: test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
      	*    HEAD:refs/heads/baz    [new branch]
    - 	*    HEAD:refs/for/next/topic    [new reference]
    + 	*    HEAD:refs/for/next/topic2    [new reference]
      	*    HEAD:refs/heads/foo    [new branch]
     -	*    HEAD:refs/for/master/topic    [new reference]
     +	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
    + 	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
    + 	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
      	Done
    - 	EOF
    - 	test_cmp expect actual &&
     
      ## transport-helper.c ##
     @@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
4:  5c9b732a38 ! 4:  11b917121f receive-pack: feed report options to post-receive
    @@ builtin/receive-pack.c: static int feed_receive_hook(void *state_, const char **
      		*bufp = state->buf.buf;
      		*sizep = state->buf.len;
     
    + ## t/t5411/once-0010-report-status-v1.sh ##
    +@@ t/t5411/once-0010-report-status-v1.sh: test_expect_success "proc-receive: report status v1" '
    + 	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
    + 	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
    + 	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
    +-	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
    ++	post-receive< <ZERO-OID> <COMMIT-A> refs/for/changes/23/123/1
    ++	post-receive< <COMMIT-B> <COMMIT-A> refs/for/changes/24/124/2
    + 	EOF
    + 	test_cmp expect actual &&
    + 
    +
      ## t/t5411/test-0032-report-with-options.sh ##
     @@ t/t5411/test-0032-report-with-options.sh: test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
      	remote: proc-receive> ok refs/for/master/topic
    @@ t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh: test_expect_su
      ## t/t5411/test-0038-report-mixed-refs.sh ##
     @@ t/t5411/test-0038-report-mixed-refs.sh: test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    - 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     -	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
    @@ t/t5411/test-0038-report-mixed-refs.sh: test_expect_success "proc-receive: repor
      ## t/t5411/test-0039-report-mixed-refs--porcelain.sh ##
     @@ t/t5411/test-0039-report-mixed-refs--porcelain.sh: test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
    - 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
    + 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
      	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
     -	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
     +	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
5:  cf0774127f = 5:  7b84236103 refs.c: refactor to reuse ref_is_hidden()
6:  8053b06b09 ! 6:  9d9032285b receive-pack: new config receive.procReceiveRefs
    @@ builtin/receive-pack.c: int cmd_receive_pack(int argc, const char **argv, const
      	return 0;
      }
     
    + ## t/t5411/once-0010-report-status-v1.sh ##
    +@@
    ++test_expect_success "setup receive.procReceiveRefs" '
    ++	git -C "$upstream" config --add receive.procReceiveRefs refs/for
    ++'
    ++
    + test_expect_success "setup proc-receive hook" '
    + 	write_script "$upstream/hooks/proc-receive" <<-EOF
    + 	printf >&2 "# proc-receive hook\n"
    +@@ t/t5411/once-0010-report-status-v1.sh: test_expect_success "proc-receive: report status v1" '
    + test_expect_success "cleanup" '
    + 	(
    + 		cd "$upstream" &&
    ++		git config --unset receive.procReceiveRefs &&
    + 		rm -f "hooks/proc-receive" &&
    + 		git update-ref refs/heads/master $A &&
    + 		git update-ref -d refs/for/master/topic1 &&
    +
      ## t/t5411/test-0010-proc-receive-settings.sh (new) ##
     @@
     +test_expect_success "add two receive.procReceiveRefs settings" '
7:  861b21d471 = 7:  1a3f58652d doc: add documentation for the proc-receive hook

----

Jiang Xin (7):
  transport: not report a non-head push as a branch
  receive-pack: add new proc-receive hook
  New capability "report-status-v2" for git-push
  receive-pack: feed report options to post-receive
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt              |  14 +
 Documentation/githooks.txt                    |  62 +++
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 461 ++++++++++++++++--
 builtin/send-pack.c                           |  21 +-
 refs.c                                        |  13 +-
 refs.h                                        |   1 +
 remote.c                                      |   5 +-
 remote.h                                      |  16 +-
 send-pack.c                                   |  98 +++-
 t/helper/test-proc-receive.c                  | 175 +++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411-proc-receive-hook.sh                  | 114 +++++
 t/t5411/common-functions.sh                   |  56 +++
 t/t5411/once-0010-report-status-v1.sh         | 101 ++++
 t/t5411/test-0000-standard-git-push.sh        |  84 ++++
 .../test-0001-standard-git-push--porcelain.sh |  86 ++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0011-no-hook-error.sh            |  64 +++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 +++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++
 t/t5411/test-0020-report-ng.sh                |  67 +++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 +++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 ++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 ++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 +++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 ++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 ++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 ++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 ++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 169 +++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 172 +++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 ++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 ++++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 +++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport-helper.c                            |  53 +-
 transport.c                                   | 121 +++--
 48 files changed, 3859 insertions(+), 94 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/once-0010-report-status-v1.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v15 1/7] transport: not report a non-head push as a branch
  2020-05-06 23:14                                   ` Junio C Hamano
  2020-05-07  1:37                                     ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 2/7] receive-pack: add new proc-receive hook Jiang Xin
                                                       ` (5 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh                  | 108 ++++++++++++++++++
 t/t5411/common-functions.sh                   |  56 +++++++++
 t/t5411/test-0000-standard-git-push.sh        |  84 ++++++++++++++
 .../test-0001-standard-git-push--porcelain.sh |  86 ++++++++++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++++++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport.c                                   |   9 +-
 8 files changed, 408 insertions(+), 4 deletions(-)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..8a1158cc1e
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+setup_upstream_and_workbench () {
+	# Refs of upstream : master(A)
+	# Refs of workbench: master(A)  tags/v123
+	test_expect_success "setup upstream and workbench" '
+		rm -rf upstream.git &&
+		rm -rf workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git remote add origin ../upstream.git &&
+			git update-ref refs/heads/master $A &&
+			git tag -m "v123" v123 $A &&
+			git push origin $B:refs/heads/master &&
+			git -C ../upstream.git update-ref \
+				refs/heads/master $A
+		) &&
+		TAG=$(git -C workbench rev-parse v123) &&
+
+		# setup pre-receive hook
+		write_script upstream.git/hooks/pre-receive <<-\EOF &&
+		exec >&2
+		echo "# pre-receive hook"
+		while read old new ref
+		do
+			echo "pre-receive< $old $new $ref"
+		done
+		EOF
+
+		# setup post-receive hook
+		write_script upstream.git/hooks/post-receive <<-\EOF &&
+		exec >&2
+		echo "# post-receive hook"
+		while read old new ref
+		do
+			echo "post-receive< $old $new $ref"
+		done
+		EOF
+
+		upstream=upstream.git
+	'
+}
+
+run_proc_receive_hook_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	local)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Include test cases for both file and HTTP protocol
+	for t in  "$TEST_DIRECTORY"/t5411/test-*.sh
+	do
+		. "$t"
+	done
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run test cases for 'proc-receive' hook on local file protocol.
+run_proc_receive_hook_test local
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup for HTTP protocol" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	set_askpass user@host pass@host &&
+	git -C workbench remote set-url origin $HTTPD_URL/smart/upstream.git
+'
+
+setup_askpass_helper
+
+# Run test cases for 'proc-receive' hook on HTTP protocol.
+run_proc_receive_hook_test http
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6580bebd8e
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,56 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
new file mode 100644
index 0000000000..52160aebf7
--- /dev/null
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -0,0 +1,84 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> next
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] HEAD -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
new file mode 100644
index 0000000000..4077015b17
--- /dev/null
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -0,0 +1,86 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/next    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		HEAD:refs/heads/master \
+		HEAD:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	-    :refs/heads/next    [deleted]
+	*    refs/tags/v123:refs/tags/v123    [new tag]
+	*    HEAD:refs/review/master/topic    [new reference]
+	*    HEAD:refs/heads/a/b/c    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
new file mode 100644
index 0000000000..c246f7e68e
--- /dev/null
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -0,0 +1,33 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (pre-receive hook declined)
+	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
new file mode 100644
index 0000000000..510cd3b333
--- /dev/null
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (pre-receive hook declined)
+	!    HEAD:refs/heads/next    [remote rejected] (pre-receive hook declined)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9c6218f568..77938db77f 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 15f5ba4e8f..26fefd13c1 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v15 2/7] receive-pack: add new proc-receive hook
  2020-05-06 23:14                                   ` Junio C Hamano
                                                       ` (2 preceding siblings ...)
  2020-05-07 16:10                                     ` [PATCH v15 1/7] transport: not report a non-head push as a branch Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 3/7] New capability "report-status-v2" for git-push Jiang Xin
                                                       ` (4 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option fall-through)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status can be given in options
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option refname <refname>)
    H: PKT-LINE(option old-oid <old-oid>)
    H: PKT-LINE(option new-oid <new-oid>)
    H: PKT-LINE(option forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in option lines.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" will be
extended in latter commit just like what the "proc-receive" hook reports
to "receive-pack".

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 353 ++++++++++++++++--
 remote.h                                      |  14 +
 t/helper/test-proc-receive.c                  | 175 +++++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411/test-0011-no-hook-error.sh            |  64 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 ++++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++++
 t/t5411/test-0020-report-ng.sh                |  67 ++++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 ++++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 +++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 +++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 ++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 +++++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 +++++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 +++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 +++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 159 ++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 162 ++++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 +++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 +++++
 28 files changed, 2659 insertions(+), 32 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh

diff --git a/Makefile b/Makefile
index 3d3a39fc19..607aeabeee 100644
--- a/Makefile
+++ b/Makefile
@@ -722,6 +722,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a00f91c1a0..23206a3c26 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
-	const char *error_string;
+	struct ref_push_report report;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -773,7 +776,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	struct command *cmd = state->cmd;
 
 	while (cmd &&
-	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
+	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
@@ -838,6 +841,268 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	int code = 0;
+	int new_options = 1;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *head;
+		const char *refname;
+		char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head);
+			code = -1;
+			continue;
+		}
+		*p++ = '\0';
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
+					new_options = 0;
+				}
+				code = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			else if (!strcmp(key, "fall-through"))
+				/* Fall through, let 'receive-pack' to execute it. */
+				hint->run_proc_receive = 0;
+			continue;
+		}
+
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    head, refname);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!strcmp(head, "ng")) {
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+			code = -1;
+		}
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		new_options = 1;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->report.error_message &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->report.error_message = "proc-receive failed to report status";
+		    code = -1;
+		}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int hook_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					hook_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->report.error_message)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (hook_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1201,7 +1466,7 @@ static void run_update_post_hook(struct command *commands)
 		return;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string || cmd->did_not_exist)
+		if (cmd->report.error_message || cmd->did_not_exist)
 			continue;
 		if (!proc.args.argc)
 			argv_array_push(&proc.args, hook);
@@ -1235,7 +1500,7 @@ static void check_aliased_update_internal(struct command *cmd,
 	if (!dst_name) {
 		rp_error("refusing update to broken symref '%s'", cmd->ref_name);
 		cmd->skip_update = 1;
-		cmd->error_string = "broken symref";
+		cmd->report.error_message = "broken symref";
 		return;
 	}
 	dst_name = strip_namespace(dst_name);
@@ -1262,7 +1527,7 @@ static void check_aliased_update_internal(struct command *cmd,
 		 find_unique_abbrev(&dst_cmd->old_oid, DEFAULT_ABBREV),
 		 find_unique_abbrev(&dst_cmd->new_oid, DEFAULT_ABBREV));
 
-	cmd->error_string = dst_cmd->error_string =
+	cmd->report.error_message = dst_cmd->report.error_message =
 		"inconsistent aliased update";
 }
 
@@ -1291,7 +1556,7 @@ static void check_aliased_updates(struct command *commands)
 	string_list_sort(&ref_list);
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			check_aliased_update(cmd, &ref_list);
 	}
 
@@ -1328,7 +1593,7 @@ static void set_connectivity_errors(struct command *commands,
 				     &opt))
 			continue;
 
-		cmd->error_string = "missing necessary objects";
+		cmd->report.error_message = "missing necessary objects";
 	}
 }
 
@@ -1367,7 +1632,7 @@ static void reject_updates_to_hidden(struct command *commands)
 	prefix_len = refname_full.len;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			continue;
 
 		strbuf_setlen(&refname_full, prefix_len);
@@ -1376,9 +1641,9 @@ static void reject_updates_to_hidden(struct command *commands)
 		if (!ref_is_hidden(cmd->ref_name, refname_full.buf))
 			continue;
 		if (is_null_oid(&cmd->new_oid))
-			cmd->error_string = "deny deleting a hidden ref";
+			cmd->report.error_message = "deny deleting a hidden ref";
 		else
-			cmd->error_string = "deny updating a hidden ref";
+			cmd->report.error_message = "deny updating a hidden ref";
 	}
 
 	strbuf_release(&refname_full);
@@ -1386,7 +1651,7 @@ static void reject_updates_to_hidden(struct command *commands)
 
 static int should_process_cmd(struct command *cmd)
 {
-	return !cmd->error_string && !cmd->skip_update;
+	return !cmd->report.error_message && !cmd->skip_update;
 }
 
 static void warn_if_skipped_connectivity_check(struct command *commands,
@@ -1413,24 +1678,24 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
 		if (!transaction) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "transaction failed to start";
+			cmd->report.error_message = "transaction failed to start";
 			continue;
 		}
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (!cmd->error_string
+		if (!cmd->report.error_message
 		    && ref_transaction_commit(transaction, &err)) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "failed to update ref";
+			cmd->report.error_message = "failed to update ref";
 		}
 		ref_transaction_free(transaction);
 	}
@@ -1453,12 +1718,12 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			goto failure;
 	}
 
@@ -1471,8 +1736,8 @@ static void execute_commands_atomic(struct command *commands,
 
 failure:
 	for (cmd = commands; cmd; cmd = cmd->next)
-		if (!cmd->error_string)
-			cmd->error_string = reported_error;
+		if (!cmd->report.error_message)
+			cmd->report.error_message = reported_error;
 
 cleanup:
 	ref_transaction_free(transaction);
@@ -1489,10 +1754,11 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
-			cmd->error_string = "unpacker error";
+			cmd->report.error_message = "unpacker error";
 		return;
 	}
 
@@ -1518,10 +1784,25 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "pre-receive hook declined";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "pre-receive hook declined";
 		}
 		return;
 	}
@@ -1532,8 +1813,8 @@ static void execute_commands(struct command *commands,
 	 */
 	if (tmp_objdir_migrate(tmp_objdir) < 0) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "unable to migrate objects to permanent storage";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "unable to migrate objects to permanent storage";
 		}
 		return;
 	}
@@ -1544,6 +1825,14 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->report.error_message &&
+			    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED) &&
+			    (cmd->run_proc_receive || use_atomic))
+				cmd->report.error_message = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -1905,7 +2194,7 @@ static void update_shallow_info(struct command *commands,
 		if (is_null_oid(&cmd->new_oid))
 			continue;
 		if (ref_status[cmd->index]) {
-			cmd->error_string = "shallow update not allowed";
+			cmd->report.error_message = "shallow update not allowed";
 			cmd->skip_update = 1;
 		}
 	}
@@ -1920,12 +2209,12 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			packet_buf_write(&buf, "ok %s\n",
-					 cmd->ref_name);
+						 cmd->ref_name);
 		else
 			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+					 cmd->ref_name, cmd->report.error_message);
 	}
 	packet_buf_flush(&buf);
 
@@ -2029,7 +2318,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		if (!check_cert_push_options(&push_options)) {
 			struct command *cmd;
 			for (cmd = commands; cmd; cmd = cmd->next)
-				cmd->error_string = "inconsistent push options";
+				cmd->report.error_message = "inconsistent push options";
 		}
 
 		prepare_shallow_info(&si, &shallow);
diff --git a/remote.h b/remote.h
index 11d8719b58..2ed2a16e16 100644
--- a/remote.h
+++ b/remote.h
@@ -93,6 +93,20 @@ int for_each_remote(each_remote_fn fn, void *priv);
 
 int remote_has_url(struct remote *remote, const char *url);
 
+struct ref_push_report_options {
+	char *ref_name;
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	unsigned int forced_update:1;
+	struct ref_push_report_options *next;
+};
+
+struct ref_push_report {
+	const char *message;
+	const char *error_message;
+	struct ref_push_report_options *options;
+};
+
 struct ref {
 	struct ref *next;
 	struct object_id old_oid;
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..6ace1863b0
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,175 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "string-list.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int die_version = 0;
+static int die_readline = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static int verbose = 0;
+static int version = 1;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1 || die_version)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ' ||
+		    die_readline)
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_BOOL(0, "die-version", &die_version,
+			 "die during version negotiation"),
+		OPT_BOOL(0, "die-readline", &die_readline,
+			 "die when readline"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 590b2efca7..a0d3966b29 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -46,6 +46,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ddc8e990e9..07034d3f38 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -35,6 +35,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
new file mode 100644
index 0000000000..bb6ec92a92
--- /dev/null
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -0,0 +1,64 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
new file mode 100644
index 0000000000..de3a2cebef
--- /dev/null
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -0,0 +1,66 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
new file mode 100644
index 0000000000..c5fe4cb37b
--- /dev/null
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -0,0 +1,217 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: bad protocol version: 1
+	remote: error: proc-receive version "0" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
new file mode 100644
index 0000000000..e56a76a5ac
--- /dev/null
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -0,0 +1,160 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
new file mode 100644
index 0000000000..f726b7ca9c
--- /dev/null
+++ b/t/t5411/test-0020-report-ng.sh
@@ -0,0 +1,67 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
new file mode 100644
index 0000000000..2defe93bc9
--- /dev/null
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -0,0 +1,69 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
new file mode 100644
index 0000000000..92a415b929
--- /dev/null
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
new file mode 100644
index 0000000000..d3a5b421ee
--- /dev/null
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -0,0 +1,46 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
new file mode 100644
index 0000000000..c3946f329a
--- /dev/null
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
new file mode 100644
index 0000000000..b2dd4a4173
--- /dev/null
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
new file mode 100644
index 0000000000..d0c4da8b23
--- /dev/null
+++ b/t/t5411/test-0026-push-options.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
new file mode 100644
index 0000000000..c3461232b8
--- /dev/null
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -0,0 +1,82 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push \
+		--porcelain \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
new file mode 100644
index 0000000000..44c99d3831
--- /dev/null
+++ b/t/t5411/test-0030-report-ok.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
new file mode 100644
index 0000000000..58877ef914
--- /dev/null
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -0,0 +1,36 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
new file mode 100644
index 0000000000..a743aa8018
--- /dev/null
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -0,0 +1,256 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
new file mode 100644
index 0000000000..8a50d7efe3
--- /dev/null
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -0,0 +1,265 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/for/a/b/c/topic    [new reference]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
new file mode 100644
index 0000000000..aca2b0676c
--- /dev/null
+++ b/t/t5411/test-0034-report-ft.sh
@@ -0,0 +1,44 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
new file mode 100644
index 0000000000..114a14b544
--- /dev/null
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (fall-through, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    <COMMIT-B>:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
new file mode 100644
index 0000000000..12acf9ea1f
--- /dev/null
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -0,0 +1,159 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
new file mode 100644
index 0000000000..57dc0d17ac
--- /dev/null
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -0,0 +1,162 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
new file mode 100644
index 0000000000..1dd6a97a97
--- /dev/null
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -0,0 +1,89 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic2
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
+	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
new file mode 100644
index 0000000000..eaa9c61ae3
--- /dev/null
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -0,0 +1,91 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/bar    [new branch]
+	*    HEAD:refs/heads/baz    [new branch]
+	*    HEAD:refs/for/next/topic2    [new reference]
+	*    HEAD:refs/heads/foo    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
+	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v15 3/7] New capability "report-status-v2" for git-push
  2020-05-06 23:14                                   ` Junio C Hamano
                                                       ` (3 preceding siblings ...)
  2020-05-07 16:10                                     ` [PATCH v15 2/7] receive-pack: add new proc-receive hook Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 4/7] receive-pack: feed report options to post-receive Jiang Xin
                                                       ` (3 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The new introduced "proc-receive" hook may handle a command for a
pseudo-reference with a zero-old as its old-oid, while the hook may
create or update a reference with different name, different new-oid,
and different old-oid (the reference may exist already with a non-zero
old-oid).  Current "report-status" protocol cannot report the status for
such reference rewrite.

Add new capability "report-status-v2" and new report protocol which is
not backward compatible for report of git-push.

If a user pushes to a pseudo-reference "refs/for/master/topic", and
"receive-pack" creates two new references "refs/changes/23/123/1" and
"refs/changes/24/124/1", for client without the knowledge of
"report-status-v2", "receive-pack" will only send "ok/ng" directives in
the report, such as:

    ok ref/for/master/topic

But for client which has the knowledge of "report-status-v2",
"receive-pack" will use "option" directives to report more attributes
for the reference given by the above "ok/ng" directive.

    ok refs/for/master/topic
    option refname refs/changes/23/123/1
    option new-oid <new-oid>
    ok refs/for/master/topic
    option refname refs/changes/24/124/1
    option new-oid <new-oid>

The client will report two new created references to the end user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        |  52 +++++++-
 builtin/send-pack.c                           |  21 +++-
 remote.c                                      |   5 +-
 remote.h                                      |   2 +-
 send-pack.c                                   |  98 ++++++++++++---
 t/t5411-proc-receive-hook.sh                  |   6 +
 t/t5411/once-0010-report-status-v1.sh         |  95 ++++++++++++++
 t/t5411/test-0032-report-with-options.sh      |  14 +--
 ...est-0033-report-with-options--porcelain.sh |  14 +--
 ...t-0036-report-multi-rewrite-for-one-ref.sh |  11 +-
 ...rt-multi-rewrite-for-one-ref--porcelain.sh |  11 +-
 t/t5411/test-0038-report-mixed-refs.sh        |   2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |   2 +-
 transport-helper.c                            |  53 +++++++-
 transport.c                                   | 118 +++++++++++++-----
 15 files changed, 428 insertions(+), 76 deletions(-)
 create mode 100644 t/t5411/once-0010-report-status-v1.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 23206a3c26..d142bab54f 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -56,6 +56,7 @@ static int advertise_push_options;
 static int unpack_limit = 100;
 static off_t max_input_size;
 static int report_status;
+static int report_status_v2;
 static int use_sideband;
 static int use_atomic;
 static int use_push_options;
@@ -239,7 +240,7 @@ static void show_ref(const char *path, const struct object_id *oid)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status report-status-v2 delete-refs side-band-64k quiet");
 		if (advertise_atomic_push)
 			strbuf_addstr(&cap, " atomic");
 		if (prefer_ofs_delta)
@@ -1914,6 +1915,8 @@ static struct command *read_head_info(struct packet_reader *reader,
 			const char *feature_list = reader->line + linelen + 1;
 			if (parse_feature_request(feature_list, "report-status"))
 				report_status = 1;
+			if (parse_feature_request(feature_list, "report-status-v2"))
+				report_status_v2 = 1;
 			if (parse_feature_request(feature_list, "side-band-64k"))
 				use_sideband = LARGE_PACKET_MAX;
 			if (parse_feature_request(feature_list, "quiet"))
@@ -2225,6 +2228,49 @@ static void report(struct command *commands, const char *unpack_status)
 	strbuf_release(&buf);
 }
 
+static void report_v2(struct command *commands, const char *unpack_status)
+{
+	struct command *cmd;
+	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
+
+	packet_buf_write(&buf, "unpack %s\n",
+			 unpack_status ? unpack_status : "ok");
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		int count = 0;
+
+		if (!cmd->report.error_message)
+			packet_buf_write(&buf, "ok %s\n",
+					 cmd->ref_name);
+		else
+			packet_buf_write(&buf, "ng %s %s\n",
+					 cmd->ref_name,
+					 cmd->report.error_message);
+		for (options = cmd->report.options; options; options = options->next) {
+			if (count++ > 0)
+				packet_buf_write(&buf, "ok %s\n", cmd->ref_name);
+			if (options->ref_name)
+				packet_buf_write(&buf, "option refname %s\n",
+						 options->ref_name);
+			if (options->old_oid)
+				packet_buf_write(&buf, "option old-oid %s\n",
+						 oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				packet_buf_write(&buf, "option new-oid %s\n",
+						 oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				packet_buf_write(&buf, "option forced-update\n");
+		}
+	}
+	packet_buf_flush(&buf);
+
+	if (use_sideband)
+		send_sideband(1, 1, buf.buf, buf.len, use_sideband);
+	else
+		write_or_die(1, buf.buf, buf.len);
+	strbuf_release(&buf);
+}
+
 static int delete_only(struct command *commands)
 {
 	struct command *cmd;
@@ -2333,7 +2379,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
-		if (report_status)
+		if (report_status_v2)
+			report_v2(commands, unpack_status);
+		else if (report_status)
 			report(commands, unpack_status);
 		run_receive_hook(commands, "post-receive", 1,
 				 &push_options);
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 2b9610f121..d3a0cd2e91 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -29,10 +29,12 @@ static struct send_pack_args args;
 static void print_helper_status(struct ref *ref)
 {
 	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
 
 	for (; ref; ref = ref->next) {
 		const char *msg = NULL;
 		const char *res;
+		int count = 0;
 
 		switch(ref->status) {
 		case REF_STATUS_NONE:
@@ -86,14 +88,29 @@ static void print_helper_status(struct ref *ref)
 
 		strbuf_reset(&buf);
 		strbuf_addf(&buf, "%s %s", res, ref->name);
-		if (ref->remote_status)
-			msg = ref->remote_status;
+		if (ref->report.error_message)
+			msg = ref->report.error_message;
 		if (msg) {
 			strbuf_addch(&buf, ' ');
 			quote_two_c_style(&buf, "", msg, 0);
 		}
 		strbuf_addch(&buf, '\n');
 
+		for (options = ref->report.options; options; options = options->next) {
+			if (count++ > 0)
+				strbuf_addf(&buf, "ok %s\n", ref->name);
+			if (options->ref_name)
+				strbuf_addf(&buf, "option refname %s\n",
+						  options->ref_name);
+			if (options->old_oid)
+				strbuf_addf(&buf, "option old-oid %s\n",
+						  oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				strbuf_addf(&buf, "option new-oid %s\n",
+						  oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				strbuf_addstr(&buf, "option forced-update\n");
+		}
 		write_or_die(1, buf.buf, buf.len);
 	}
 	strbuf_release(&buf);
diff --git a/remote.c b/remote.c
index 534c6426f1..6fd175ba1d 100644
--- a/remote.c
+++ b/remote.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "config.h"
+#include "connect.h"
 #include "remote.h"
 #include "refs.h"
 #include "refspec.h"
@@ -789,7 +790,7 @@ struct ref *copy_ref(const struct ref *ref)
 	memcpy(cpy, ref, len);
 	cpy->next = NULL;
 	cpy->symref = xstrdup_or_null(ref->symref);
-	cpy->remote_status = xstrdup_or_null(ref->remote_status);
+	cpy->report.error_message = xstrdup_or_null(ref->report.error_message);
 	cpy->peer_ref = copy_ref(ref->peer_ref);
 	return cpy;
 }
@@ -811,7 +812,7 @@ void free_one_ref(struct ref *ref)
 	if (!ref)
 		return;
 	free_one_ref(ref->peer_ref);
-	free(ref->remote_status);
+	free((void *)ref->report.error_message);
 	free(ref->symref);
 	free(ref);
 }
diff --git a/remote.h b/remote.h
index 2ed2a16e16..caa3d5c9ba 100644
--- a/remote.h
+++ b/remote.h
@@ -153,7 +153,7 @@ struct ref {
 		REF_STATUS_EXPECTING_REPORT,
 		REF_STATUS_ATOMIC_PUSH_FAILED
 	} status;
-	char *remote_status;
+	struct ref_push_report report;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
 };
diff --git a/send-pack.c b/send-pack.c
index d1b7edc995..d0ebbb0551 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -153,25 +153,80 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 {
 	struct ref *hint;
 	int ret;
+	int new_options = 1;
 
 	hint = NULL;
 	ret = receive_unpack_status(reader);
 	while (1) {
+		struct object_id old_oid, new_oid;
+		const char *head;
 		const char *refname;
-		char *msg;
+		char *p;
+
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
-		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
-			error("invalid ref status from remote: %s", reader->line);
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			error("invalid status line from remote: %s", reader->line);
 			ret = -1;
 			break;
 		}
+		*p++ = '\0';
+
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					error("'option' without a matching 'ok/ng' directive");
+					new_options = 0;
+				}
+				ret = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			continue;
+		}
 
-		refname = reader->line + 3;
-		msg = strchr(refname, ' ');
-		if (msg)
-			*msg++ = '\0';
-
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			error("invalid ref status from remote: %s", head);
+			ret = -1;
+			break;
+		}
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
 			hint = find_ref_by_name(hint, refname);
@@ -182,19 +237,24 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 					refname);
 			continue;
 		}
-		if (hint->status != REF_STATUS_EXPECTING_REPORT) {
+		if (hint->status != REF_STATUS_EXPECTING_REPORT &&
+		    hint->status != REF_STATUS_OK &&
+		    hint->status != REF_STATUS_REMOTE_REJECT) {
 			warning("remote reported status on unexpected ref: %s",
 					refname);
 			continue;
 		}
-
-		if (reader->line[0] == 'o' && reader->line[1] == 'k')
-			hint->status = REF_STATUS_OK;
-		else
+		if (!strcmp(head, "ng")) {
 			hint->status = REF_STATUS_REMOTE_REJECT;
-		hint->remote_status = xstrdup_or_null(msg);
-		/* start our next search from the next ref */
-		hint = hint->next;
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+		} else {
+			hint->status = REF_STATUS_OK;
+			hint->report.message = xstrdup_or_null(p);
+		}
+		new_options = 1;
 	}
 	return ret;
 }
@@ -371,6 +431,8 @@ int send_pack(struct send_pack_args *args,
 	/* Does the other end support the reporting? */
 	if (server_supports("report-status"))
 		status_report = 1;
+	if (server_supports("report-status-v2"))
+		status_report = 2;
 	if (server_supports("delete-refs"))
 		allow_deleting_refs = 1;
 	if (server_supports("ofs-delta"))
@@ -418,8 +480,10 @@ int send_pack(struct send_pack_args *args,
 
 	use_push_options = push_options_supported && args->push_options;
 
-	if (status_report)
+	if (status_report == 1)
 		strbuf_addstr(&cap_buf, " report-status");
+	else if (status_report == 2)
+		strbuf_addstr(&cap_buf, " report-status-v2");
 	if (use_sideband)
 		strbuf_addstr(&cap_buf, " side-band-64k");
 	if (quiet_supported && (args->quiet || !args->progress))
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 8a1158cc1e..822b63f778 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -78,6 +78,12 @@ run_proc_receive_hook_test() {
 # Initialize the upstream repository and local workbench.
 setup_upstream_and_workbench
 
+# Load test cases that only need to be executed once.
+for t in  "$TEST_DIRECTORY"/t5411/once-*.sh
+do
+	. "$t"
+done
+
 # Run test cases for 'proc-receive' hook on local file protocol.
 run_proc_receive_hook_test local
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
new file mode 100644
index 0000000000..77f84fdc5e
--- /dev/null
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -0,0 +1,95 @@
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic1" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/23/123/1" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/24/124/2" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update" \
+		-r "ng refs/for/next/topic target branch not exist"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic1(A)  foo(A)  refs/for/next/topic(A)  refs/for/master/topic2(A)
+test_expect_success "proc-receive: report status v1" '
+	{
+		printf "%s %s refs/heads/master\0report-status\n" \
+			$A $B | packetize &&
+		printf "%s %s refs/for/master/topic1\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/heads/foo\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/next/topic\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/master/topic2\n" \
+			$ZERO_OID $A | packetize &&
+		printf 0000 &&
+		printf "" | git -C "$upstream" pack-objects --stdout
+	} | git receive-pack "$upstream" --stateless-rpc \
+	>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	# pre-receive hook
+	pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	# proc-receive hook
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	proc-receive> ok refs/for/master/topic1
+	proc-receive> option fall-through
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/23/123/1
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/24/124/2
+	proc-receive> option old-oid <COMMIT-B>
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> option forced-update
+	proc-receive> ng refs/for/next/topic target branch not exist
+	000eunpack ok
+	0019ok refs/heads/master
+	001eok refs/for/master/topic1
+	0016ok refs/heads/foo
+	0033ng refs/for/next/topic target branch not exist
+	001eok refs/for/master/topic2
+	0000# post-receive hook
+	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/for/master/topic1
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  refs/for/master/topic1(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup" '
+	(
+		cd "$upstream" &&
+		rm -f "hooks/proc-receive" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/for/master/topic1 &&
+		git update-ref -d refs/heads/foo
+	)
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a743aa8018..3742ed3c3d 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -56,7 +56,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -89,7 +89,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -123,7 +123,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -155,7 +155,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -189,7 +189,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -241,9 +241,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 8a50d7efe3..929c46ccd8 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -57,7 +57,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -92,7 +92,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -127,7 +127,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -160,7 +160,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -195,7 +195,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual
@@ -249,9 +249,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
-	*    HEAD:refs/for/master/topic    [new reference]
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 12acf9ea1f..64662afdc6 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -147,7 +151,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index 57dc0d17ac..a371c8d610 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/23/123/1    [new reference]
+	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 1dd6a97a97..54bec4c026 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic2
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index eaa9c61ae3..f072d85bc4 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	*    HEAD:refs/heads/baz    [new branch]
 	*    HEAD:refs/for/next/topic2    [new reference]
 	*    HEAD:refs/heads/foo    [new branch]
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
 	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
 	Done
diff --git a/transport-helper.c b/transport-helper.c
index a46afcb69d..21d6e47966 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -725,6 +725,49 @@ static int push_update_ref_status(struct strbuf *buf,
 {
 	char *refname, *msg;
 	int status, forced = 0;
+	static struct ref *hint = NULL;
+	static int new_options = 1;
+
+	if (starts_with(buf->buf, "option ")) {
+		struct ref_push_report_options *options;
+		struct object_id old_oid, new_oid;
+		const char *key, *val;
+		char *p;
+
+		if (!hint)
+			die(_("'option' without a matching 'ok/error' directive"));
+		options = hint->report.options;
+		while (options && options->next)
+			options = options->next;
+		if (new_options) {
+			if (!options) {
+				hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = hint->report.options;
+			} else {
+				options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = options->next;
+			}
+			new_options = 0;
+		}
+		assert(options);
+		key = buf->buf + 7;
+		p = strchr(key, ' ');
+		if (p)
+			*p++ = '\0';
+		val = p;
+		if (!strcmp(key, "refname"))
+			options->ref_name = xstrdup_or_null(val);
+		else if (!strcmp(key, "old-oid") && val &&
+			 !parse_oid_hex(val, &old_oid, &val))
+			options->old_oid = oiddup(&old_oid);
+		else if (!strcmp(key, "new-oid") && val &&
+			 !parse_oid_hex(val, &new_oid, &val))
+			options->new_oid = oiddup(&new_oid);
+		else if (!strcmp(key, "forced-update"))
+			options->forced_update = 1;
+		/* Not update remote namespace again. */
+		return 1;
+	}
 
 	if (starts_with(buf->buf, "ok ")) {
 		status = REF_STATUS_OK;
@@ -787,8 +830,11 @@ static int push_update_ref_status(struct strbuf *buf,
 		*ref = find_ref_by_name(remote_refs, refname);
 	if (!*ref) {
 		warning(_("helper reported unexpected status of %s"), refname);
+		hint = NULL;
 		return 1;
 	}
+	hint = *ref;
+	new_options = 1;
 
 	if ((*ref)->status != REF_STATUS_NONE) {
 		/*
@@ -801,7 +847,12 @@ static int push_update_ref_status(struct strbuf *buf,
 
 	(*ref)->status = status;
 	(*ref)->forced_update |= forced;
-	(*ref)->remote_status = msg;
+	if (msg) {
+		if (status == REF_STATUS_OK)
+			(*ref)->report.message = msg;
+		else
+			(*ref)->report.error_message = msg;
+	}
 	return !(status == REF_STATUS_OK);
 }
 
diff --git a/transport.c b/transport.c
index 26fefd13c1..2e2b7de036 100644
--- a/transport.c
+++ b/transport.c
@@ -461,13 +461,21 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
+			     struct ref_push_report_options *options,
 			     int porcelain, int summary_width)
 {
+	char *to_name;
+
+	if (options && options->ref_name)
+		to_name = options->ref_name;
+	else
+		to_name = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +489,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -493,27 +503,52 @@ static void print_ref_status(char flag, const char *summary,
 	}
 }
 
-static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
+static void print_ok_ref_status(struct ref *ref,
+				struct ref_push_report_options *options,
+				int porcelain, int summary_width)
 {
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	const char *ref_name;
+	int forced_update;
+
+	if (options && options->old_oid)
+		old_oid = options->old_oid;
+	else
+		old_oid = &ref->old_oid;
+	if (options && options->new_oid)
+		new_oid = options->new_oid;
+	else
+		new_oid = &ref->new_oid;
+	if (options && options->forced_update)
+		forced_update = options->forced_update;
+	else
+		forced_update = ref->forced_update;
+	if (options && options->ref_name)
+		ref_name = options->ref_name;
+	else
+		ref_name = ref->name;
+
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
-				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+				 options, porcelain, summary_width);
+	else if (is_null_oid(old_oid))
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(ref_name, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(ref_name, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 ref, ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
 
-		strbuf_add_unique_abbrev(&quickref, &ref->old_oid,
+		strbuf_add_unique_abbrev(&quickref, old_oid,
 					 DEFAULT_ABBREV);
-		if (ref->forced_update) {
+		if (forced_update) {
 			strbuf_addstr(&quickref, "...");
 			type = '+';
 			msg = "forced update";
@@ -522,17 +557,18 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 			type = ' ';
 			msg = NULL;
 		}
-		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
+		strbuf_add_unique_abbrev(&quickref, new_oid,
 					 DEFAULT_ABBREV);
 
 		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
-				 int porcelain, int summary_width)
+static int _print_one_push_status(struct ref *ref, const char *dest, int count,
+				  struct ref_push_report_options *options,
+				  int porcelain, int summary_width)
 {
 	if (!count) {
 		char *url = transport_anonymize_url(dest);
@@ -543,65 +579,89 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	switch(ref->status) {
 	case REF_STATUS_NONE:
 		print_ref_status('X', "[no match]", ref, NULL, NULL,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
 		print_ref_status('!', "[rejected]", ref, NULL,
 				 "remote does not support deleting refs",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
 		print_ref_status('=', "[up to date]", ref,
-				 ref->peer_ref, NULL, porcelain, summary_width);
+				 ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "non-fast-forward", porcelain, summary_width);
+				 "non-fast-forward",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "already exists", porcelain, summary_width);
+				 "already exists",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "fetch first", porcelain, summary_width);
+				 "fetch first",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "needs force", porcelain, summary_width);
+				 "needs force",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "stale info", porcelain, summary_width);
+				 "stale info",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
 				 "new shallow roots not allowed",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
 		print_ref_status('!', "[remote rejected]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
-				 ref->remote_status, porcelain, summary_width);
+				 ref->report.error_message,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
 		print_ref_status('!', "[remote failure]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "atomic push failed", porcelain, summary_width);
+				 "atomic push failed",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-		print_ok_ref_status(ref, porcelain, summary_width);
+		print_ok_ref_status(ref, options, porcelain, summary_width);
 		break;
 	}
 
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	struct ref_push_report_options *options;
+	int n = 0;
+
+	if (!ref->report.options)
+		return _print_one_push_status(ref, dest, count,
+					      NULL, porcelain, summary_width);
+
+	for (options = ref->report.options; options; options = options->next)
+		_print_one_push_status(ref, dest, count + n++,
+				       options, porcelain, summary_width);
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v15 4/7] receive-pack: feed report options to post-receive
  2020-05-06 23:14                                   ` Junio C Hamano
                                                       ` (4 preceding siblings ...)
  2020-05-07 16:10                                     ` [PATCH v15 3/7] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                                                       ` (2 subsequent siblings)
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When commands are fed to the "post-receive" hook, report options will
be parsed and the real old-oid, new-oid, reference name will feed to
the "post-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        | 29 ++++++++++++++++---
 t/t5411/once-0010-report-status-v1.sh         |  3 +-
 t/t5411/test-0032-report-with-options.sh      | 14 ++++-----
 ...est-0033-report-with-options--porcelain.sh | 14 ++++-----
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 11 +++++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 11 +++++--
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 8 files changed, 59 insertions(+), 27 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index d142bab54f..a1ae6cc17e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -775,17 +775,38 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 {
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
+	static struct ref_push_report_options *options = NULL;
 
 	while (cmd &&
 	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
+	if (!bufp)
+		return 0; /* OK, can feed something. */
 	strbuf_reset(&state->buf);
-	strbuf_addf(&state->buf, "%s %s %s\n",
-		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
-		    cmd->ref_name);
-	state->cmd = cmd->next;
+	if (!options)
+		options = cmd->report.options;
+	if (options) {
+		struct object_id *old_oid;
+		struct object_id *new_oid;
+		char *ref_name;
+
+		old_oid = options->old_oid ? options->old_oid : &cmd->old_oid;
+		new_oid = options->new_oid ? options->new_oid : &cmd->new_oid;
+		ref_name = options->ref_name ? options->ref_name : cmd->ref_name;
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(old_oid), oid_to_hex(new_oid),
+			    ref_name);
+		options = options->next;
+		if (!options)
+			state->cmd = cmd->next;
+	} else {
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
+			    cmd->ref_name);
+		state->cmd = cmd->next;
+	}
 	if (bufp) {
 		*bufp = state->buf.buf;
 		*sizep = state->buf.len;
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index 77f84fdc5e..ebabf83937 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -68,7 +68,8 @@ test_expect_success "proc-receive: report status v1" '
 	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
 	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/changes/23/123/1
+	post-receive< <COMMIT-B> <COMMIT-A> refs/for/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index 3742ed3c3d..b77b78c49f 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -54,7 +54,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -87,7 +87,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -121,7 +121,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
@@ -153,7 +153,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
@@ -187,7 +187,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
@@ -237,9 +237,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 929c46ccd8..b817d645f1 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -55,7 +55,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -90,7 +90,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -125,7 +125,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
@@ -158,7 +158,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
@@ -193,7 +193,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
@@ -245,9 +245,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 64662afdc6..27d58edd14 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 * [new reference] HEAD -> refs/changes/24/124/1
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/24/124/1
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/23/123/1
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index a371c8d610..2519c1acd8 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	*    HEAD:refs/changes/24/124/1    [new reference]
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/24/124/1    [new reference]
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
@@ -151,7 +155,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/23/123/1    [new reference]
 	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 54bec4c026..a74a2cb449 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index f072d85bc4..52bb4e0745 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
 	*    HEAD:refs/heads/bar    [new branch]
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v15 5/7] refs.c: refactor to reuse ref_is_hidden()
  2020-05-06 23:14                                   ` Junio C Hamano
                                                       ` (5 preceding siblings ...)
  2020-05-07 16:10                                     ` [PATCH v15 4/7] receive-pack: feed report options to post-receive Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 13 +++++++++----
 refs.h |  1 +
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 224ff66c7b..76a91ac0f6 100644
--- a/refs.c
+++ b/refs.c
@@ -1344,14 +1344,14 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 	return 0;
 }
 
-int ref_is_hidden(const char *refname, const char *refname_full)
+int ref_matches(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
@@ -1377,6 +1377,11 @@ int ref_is_hidden(const char *refname, const char *refname_full)
 	return 0;
 }
 
+int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_matches(hide_refs, refname, refname_full);
+}
+
 const char *find_descendant_ref(const char *dirname,
 				const struct string_list *extras,
 				const struct string_list *skip)
diff --git a/refs.h b/refs.h
index a92d2c74c8..9907dd12b0 100644
--- a/refs.h
+++ b/refs.h
@@ -727,6 +727,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_matches(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v15 6/7] receive-pack: new config receive.procReceiveRefs
  2020-05-06 23:14                                   ` Junio C Hamano
                                                       ` (6 preceding siblings ...)
  2020-05-07 16:10                                     ` [PATCH v15 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  2020-05-07 16:10                                     ` [PATCH v15 7/7] doc: add documentation for the proc-receive hook Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings match the reference names of the commands
which are sent from git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt              |  14 +++
 builtin/receive-pack.c                        |  43 +++++--
 t/t5411/once-0010-report-status-v1.sh         |   5 +
 t/t5411/test-0010-proc-receive-settings.sh    |   7 ++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++++++++++++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 ++++++++++++++++++
 6 files changed, 289 insertions(+), 7 deletions(-)
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..e7b967feeb 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running the hook "proc-receive".
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a1ae6cc17e..fc6684c1a0 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -77,6 +77,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -229,6 +230,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_append(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1810,15 +1825,26 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2319,6 +2345,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2436,5 +2464,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index ebabf83937..884dc21ad2 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -1,3 +1,7 @@
+test_expect_success "setup receive.procReceiveRefs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/for
+'
+
 test_expect_success "setup proc-receive hook" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -88,6 +92,7 @@ test_expect_success "proc-receive: report status v1" '
 test_expect_success "cleanup" '
 	(
 		cd "$upstream" &&
+		git config --unset receive.procReceiveRefs &&
 		rm -f "hooks/proc-receive" &&
 		git update-ref refs/heads/master $A &&
 		git update-ref -d refs/for/master/topic1 &&
diff --git a/t/t5411/test-0010-proc-receive-settings.sh b/t/t5411/test-0010-proc-receive-settings.sh
new file mode 100644
index 0000000000..a36809927b
--- /dev/null
+++ b/t/t5411/test-0010-proc-receive-settings.sh
@@ -0,0 +1,7 @@
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
new file mode 100644
index 0000000000..b07c999f53
--- /dev/null
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -0,0 +1,113 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
new file mode 100644
index 0000000000..7092db0a06
--- /dev/null
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -0,0 +1,114 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+	-    :refs/heads/foo    [deleted]
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v15 7/7] doc: add documentation for the proc-receive hook
  2020-05-06 23:14                                   ` Junio C Hamano
                                                       ` (7 preceding siblings ...)
  2020-05-07 16:10                                     ` [PATCH v15 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-05-07 16:10                                     ` Jiang Xin
  8 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-07 16:10 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add documentation for the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 62 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 81f2a87e88..d66154f606 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,68 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option fall-through)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status can be given in options
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option refname <refname>)
+    H: PKT-LINE(option old-oid <old-oid>)
+    H: PKT-LINE(option new-oid <new-oid>)
+    H: PKT-LINE(option forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook will use
+"option" directives to report extended attributes for the reference given
+by the above "ok/ng" directive.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v12 3/7] receive-pack: add new proc-receive hook
  2020-04-30 15:33                               ` Jiang Xin
                                                   ` (7 preceding siblings ...)
  2020-05-05 14:41                                 ` [PATCH v14 7/7] doc: add documentation for the proc-receive hook Jiang Xin
@ 2020-05-07 17:27                                 ` Jeff King
  8 siblings, 0 replies; 266+ messages in thread
From: Jeff King @ 2020-05-07 17:27 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Christian Couder, Git List, Jiang Xin

On Thu, Apr 30, 2020 at 11:33:27PM +0800, Jiang Xin wrote:

> Thanks to Peff for providing technical details of the architecture.  I
> understand that "receive-pack" of GitHub backend is not involved in
> references update (executing the commands), so the "proc-receive" hook
> won't be turned on for GitHub's architecture. While in our
> architecture (inspired by "spokes" of GitHub), the proxy will deliver
> not only packfile, but also commands to all three replicas. The proxy
> will execute "receive-pack" on the replica with a special argument, so
> the proxy can talk with "receive-pack" with an extended protocol.
> After running pre-receive hook and release the packfile from
> quarantine, the replica will stop and wait for the proxy to
> coordinate. After creating a distributed lock, the proxy will tell all
> the replicas continue to update the references.  One problem we met is
> the proc-receive and the post-receive hook must be executed once. We
> can make the execution of the hooks idempotent, or let only one of the
> replica run the hook. We choose the latter.

OK, that makes more sense. We solve that by not updating the refs at all
via receive-pack (which gives us flexibility to run our own hooks
separately on just one replica, etc).

> I agree that adding new capability (report-status-v2) is a better
> solution, but I think the above extension is a bit wordy. We add
> additional 18 characters for each "ok <ref>" status, and add
> additional 32 characters for each "ng <ref> <msg>" status. Can we
> extend it like this:
> 
>     ok <ref>
>     [optional key-value pairs]
>     ng <ref> <error message>
>     [optional key-value pairs]

I doubt the number of bytes is all that important, but certainly I'm
happy with this (and it looks like what you put into v14).

-Peff

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v16 00/11] New proc-receive hook for centralized workflow
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                           ` (10 more replies)
  2020-05-18  9:40                                       ` [PATCH v16 01/11] transport: not report a non-head push as a branch Jiang Xin
                                                         ` (10 subsequent siblings)
  11 siblings, 11 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This topic introduces a new hook named "proc-receive" for
"receive-pack".  We can use this hook to implement a Gerrit-like
centralized workflow.

## Changes since v15

1. Parse report options in function `transport_update_tracking_ref()`
   in order to update remote-tracking branches properly.

   See patch 9/11 and 10/11.

2. Add docs for 'report-status-v2' in 'Documentation/technical/
   pack-protocol.txt', and other files.

   See patch 5/11.

3. Remove unnecessary headers:

    --- a/remote.c
    +++ b/remote.c
    @@ -1,6 +1,5 @@
     #include "cache.h"
     #include "config.h"
    -#include "connect.h"
     #include "remote.h"
     #include "refs.h"
     #include "refspec.h"
    
    --- a/t/helper/test-proc-receive.c
    +++ b/t/helper/test-proc-receive.c
    @@ -3,7 +3,6 @@
     #include "parse-options.h"
     #include "pkt-line.h"
     #include "sigchain.h"
    -#include "string-list.h"
     #include "test-tool.h"
     
     static const char *proc_receive_usage[] = {


4. Fix format issues:

    --- a/builtin/receive-pack.c
    +++ b/builtin/receive-pack.c
    @@ -2262,7 +2262,7 @@ static void report(struct command *commands, const char *unpack_status)
     	for (cmd = commands; cmd; cmd = cmd->next) {
     		if (!cmd->report.error_message)
     			packet_buf_write(&buf, "ok %s\n",
    -						 cmd->ref_name);
    +					 cmd->ref_name);
     		else
     			packet_buf_write(&buf, "ng %s %s\n",
     					 cmd->ref_name, cmd->report.error_message);

    --- a/builtin/send-pack.c
    +++ b/builtin/send-pack.c
    @@ -101,13 +101,13 @@ static void print_helper_status(struct ref *ref)
     				strbuf_addf(&buf, "ok %s\n", ref->name);
     			if (options->ref_name)
     				strbuf_addf(&buf, "option refname %s\n",
    -						  options->ref_name);
    +					    options->ref_name);
     			if (options->old_oid)
     				strbuf_addf(&buf, "option old-oid %s\n",
    -						  oid_to_hex(options->old_oid));
    +					    oid_to_hex(options->old_oid));
     			if (options->new_oid)
     				strbuf_addf(&buf, "option new-oid %s\n",
    -						  oid_to_hex(options->new_oid));
    +					    oid_to_hex(options->new_oid));
     			if (options->forced_update)
     				strbuf_addstr(&buf, "option forced-update\n");
     		}

5. Other minor changes:

    --- a/builtin/receive-pack.c
    +++ b/builtin/receive-pack.c
    @@ -806,7 +806,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
     	if (options) {
     		struct object_id *old_oid;
     		struct object_id *new_oid;
    -		char *ref_name;
    +		const char *ref_name;
     
     		old_oid = options->old_oid ? options->old_oid : &cmd->old_oid;
     		new_oid = options->new_oid ? options->new_oid : &cmd->new_oid;
    
    
    --- a/remote.h
    +++ b/remote.h
    @@ -94,7 +94,7 @@ int for_each_remote(each_remote_fn fn, void *priv);
     int remote_has_url(struct remote *remote, const char *url);
     
     struct ref_push_report_options {
    -	char *ref_name;
    +	const char *ref_name;
     	struct object_id *old_oid;
     	struct object_id *new_oid;
     	unsigned int forced_update:1;
    
----

Jiang Xin (11):
  transport: not report a non-head push as a branch
  t5411: add basic test cases for proc-receive hook
  receive-pack: add new proc-receive hook
  New capability "report-status-v2" for git-push
  doc: add document for capability report-status-v2
  receive-pack: feed report options to post-receive
  refs.c: refactor to reuse ref_is_hidden()
  receive-pack: new config receive.procReceiveRefs
  t5411: test updates of remote-tracking branches
  transport: parse report options for tracking refs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt              |  14 +
 Documentation/githooks.txt                    |  62 +++
 Documentation/technical/pack-protocol.txt     |  43 +-
 .../technical/protocol-capabilities.txt       |  17 +-
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 461 ++++++++++++++++--
 builtin/send-pack.c                           |  21 +-
 refs.c                                        |  13 +-
 refs.h                                        |   1 +
 remote.c                                      |   4 +-
 remote.h                                      |  16 +-
 send-pack.c                                   |  99 +++-
 t/helper/test-proc-receive.c                  | 174 +++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411-proc-receive-hook.sh                  | 117 +++++
 t/t5411/common-functions.sh                   |  56 +++
 t/t5411/once-0010-report-status-v1.sh         |  88 ++++
 t/t5411/test-0000-standard-git-push.sh        | 143 ++++++
 .../test-0001-standard-git-push--porcelain.sh | 147 ++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0011-no-hook-error.sh            |  64 +++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 +++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++
 t/t5411/test-0020-report-ng.sh                |  67 +++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 +++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 ++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 ++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 +++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 ++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 ++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 ++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 ++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 227 +++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 172 +++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 ++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 ++++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 +++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport-helper.c                            |  53 +-
 transport.c                                   | 161 ++++--
 50 files changed, 4109 insertions(+), 111 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/once-0010-report-status-v1.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v16 01/11] transport: not report a non-head push as a branch
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 02/11] t5411: add basic test cases for proc-receive hook Jiang Xin
                                                         ` (9 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5516-fetch-push.sh | 2 +-
 transport.c           | 9 ++++++---
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9c6218f568..77938db77f 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 15f5ba4e8f..26fefd13c1 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 02/11] t5411: add basic test cases for proc-receive hook
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 01/11] transport: not report a non-head push as a branch Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 03/11] receive-pack: add new " Jiang Xin
                                                         ` (8 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Topic "proc-receive-hook" will change the workflow and output of
git-push. Add some basic test cases in t5411 before introducing the new
topic.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh                  | 108 +++++++++++++
 t/t5411/common-functions.sh                   |  56 +++++++
 t/t5411/test-0000-standard-git-push.sh        | 143 +++++++++++++++++
 .../test-0001-standard-git-push--porcelain.sh | 147 ++++++++++++++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++++
 6 files changed, 521 insertions(+)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..3a684353a8
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+setup_upstream_and_workbench () {
+	# Refs of upstream : master(A)
+	# Refs of workbench: master(A)  tags/v123
+	test_expect_success "setup upstream and workbench" '
+		rm -rf upstream.git &&
+		rm -rf workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git tag -m "v123" v123 $A &&
+			git remote add origin ../upstream.git &&
+			git push origin master &&
+			git update-ref refs/heads/master $A $B &&
+			git -C ../upstream.git update-ref \
+				refs/heads/master $A $B
+		) &&
+		TAG=$(git -C workbench rev-parse v123) &&
+
+		# setup pre-receive hook
+		write_script upstream.git/hooks/pre-receive <<-\EOF &&
+		exec >&2
+		echo "# pre-receive hook"
+		while read old new ref
+		do
+			echo "pre-receive< $old $new $ref"
+		done
+		EOF
+
+		# setup post-receive hook
+		write_script upstream.git/hooks/post-receive <<-\EOF &&
+		exec >&2
+		echo "# post-receive hook"
+		while read old new ref
+		do
+			echo "post-receive< $old $new $ref"
+		done
+		EOF
+
+		upstream=upstream.git
+	'
+}
+
+run_proc_receive_hook_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	local)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Include test cases for both file and HTTP protocol
+	for t in  "$TEST_DIRECTORY"/t5411/test-*.sh
+	do
+		. "$t"
+	done
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run test cases for 'proc-receive' hook on local file protocol.
+run_proc_receive_hook_test local
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup for HTTP protocol" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin "$HTTPD_URL/auth-push/smart/upstream.git" &&
+	set_askpass user@host pass@host
+'
+
+setup_askpass_helper
+
+# Run test cases for 'proc-receive' hook on HTTP protocol.
+run_proc_receive_hook_test http
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6580bebd8e
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,56 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
new file mode 100644
index 0000000000..e206587348
--- /dev/null
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -0,0 +1,143 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> next
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [rejected] master -> master (non-fast-forward)
+	 ! [rejected] <COMMIT-B> -> next (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> next
+	 ! [rejected] master -> master (non-fast-forward)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> master -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] master -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
new file mode 100644
index 0000000000..48f6fcc846
--- /dev/null
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -0,0 +1,147 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/next    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --atomic --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "s/^# GETTEXT POISON #//" \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	! refs/heads/master:refs/heads/master [rejected] (non-fast-forward)
+	! <COMMIT-B>:refs/heads/next [rejected] (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/next    <OID-A>..<OID-B>
+	!    refs/heads/master:refs/heads/master    [rejected] (non-fast-forward)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	+    refs/heads/master:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	-    :refs/heads/next    [deleted]
+	*    refs/tags/v123:refs/tags/v123    [new tag]
+	*    refs/heads/master:refs/review/master/topic    [new reference]
+	*    HEAD:refs/heads/a/b/c    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
new file mode 100644
index 0000000000..c246f7e68e
--- /dev/null
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -0,0 +1,33 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (pre-receive hook declined)
+	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
new file mode 100644
index 0000000000..b14894de81
--- /dev/null
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (pre-receive hook declined)
+	!    HEAD:refs/heads/next    [remote rejected] (pre-receive hook declined)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 03/11] receive-pack: add new proc-receive hook
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (2 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 02/11] t5411: add basic test cases for proc-receive hook Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 04/11] New capability "report-status-v2" for git-push Jiang Xin
                                                         ` (7 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option fall-through)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status can be given in options
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option refname <refname>)
    H: PKT-LINE(option old-oid <old-oid>)
    H: PKT-LINE(option new-oid <new-oid>)
    H: PKT-LINE(option forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in option lines.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" will be
extended in latter commit just like what the "proc-receive" hook reports
to "receive-pack".

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 351 ++++++++++++++++--
 remote.h                                      |  14 +
 t/helper/test-proc-receive.c                  | 174 +++++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411/test-0011-no-hook-error.sh            |  64 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 ++++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++++
 t/t5411/test-0020-report-ng.sh                |  67 ++++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 ++++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 +++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 +++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 ++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 +++++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 +++++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 +++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 +++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 159 ++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 162 ++++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 +++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 +++++
 28 files changed, 2657 insertions(+), 31 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh

diff --git a/Makefile b/Makefile
index 3d3a39fc19..607aeabeee 100644
--- a/Makefile
+++ b/Makefile
@@ -722,6 +722,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index ea3d0f01af..717bebd3e1 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -309,11 +309,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
-	const char *error_string;
+	struct ref_push_report report;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -774,7 +777,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	struct command *cmd = state->cmd;
 
 	while (cmd &&
-	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
+	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
@@ -839,6 +842,268 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	int code = 0;
+	int new_options = 1;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *head;
+		const char *refname;
+		char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head);
+			code = -1;
+			continue;
+		}
+		*p++ = '\0';
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
+					new_options = 0;
+				}
+				code = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			else if (!strcmp(key, "fall-through"))
+				/* Fall through, let 'receive-pack' to execute it. */
+				hint->run_proc_receive = 0;
+			continue;
+		}
+
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    head, refname);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!strcmp(head, "ng")) {
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+			code = -1;
+		}
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		new_options = 1;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->report.error_message &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->report.error_message = "proc-receive failed to report status";
+		    code = -1;
+		}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int hook_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					hook_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->report.error_message)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (hook_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1202,7 +1467,7 @@ static void run_update_post_hook(struct command *commands)
 		return;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string || cmd->did_not_exist)
+		if (cmd->report.error_message || cmd->did_not_exist)
 			continue;
 		if (!proc.args.argc)
 			argv_array_push(&proc.args, hook);
@@ -1236,7 +1501,7 @@ static void check_aliased_update_internal(struct command *cmd,
 	if (!dst_name) {
 		rp_error("refusing update to broken symref '%s'", cmd->ref_name);
 		cmd->skip_update = 1;
-		cmd->error_string = "broken symref";
+		cmd->report.error_message = "broken symref";
 		return;
 	}
 	dst_name = strip_namespace(dst_name);
@@ -1263,7 +1528,7 @@ static void check_aliased_update_internal(struct command *cmd,
 		 find_unique_abbrev(&dst_cmd->old_oid, DEFAULT_ABBREV),
 		 find_unique_abbrev(&dst_cmd->new_oid, DEFAULT_ABBREV));
 
-	cmd->error_string = dst_cmd->error_string =
+	cmd->report.error_message = dst_cmd->report.error_message =
 		"inconsistent aliased update";
 }
 
@@ -1292,7 +1557,7 @@ static void check_aliased_updates(struct command *commands)
 	string_list_sort(&ref_list);
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			check_aliased_update(cmd, &ref_list);
 	}
 
@@ -1329,7 +1594,7 @@ static void set_connectivity_errors(struct command *commands,
 				     &opt))
 			continue;
 
-		cmd->error_string = "missing necessary objects";
+		cmd->report.error_message = "missing necessary objects";
 	}
 }
 
@@ -1368,7 +1633,7 @@ static void reject_updates_to_hidden(struct command *commands)
 	prefix_len = refname_full.len;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			continue;
 
 		strbuf_setlen(&refname_full, prefix_len);
@@ -1377,9 +1642,9 @@ static void reject_updates_to_hidden(struct command *commands)
 		if (!ref_is_hidden(cmd->ref_name, refname_full.buf))
 			continue;
 		if (is_null_oid(&cmd->new_oid))
-			cmd->error_string = "deny deleting a hidden ref";
+			cmd->report.error_message = "deny deleting a hidden ref";
 		else
-			cmd->error_string = "deny updating a hidden ref";
+			cmd->report.error_message = "deny updating a hidden ref";
 	}
 
 	strbuf_release(&refname_full);
@@ -1387,7 +1652,7 @@ static void reject_updates_to_hidden(struct command *commands)
 
 static int should_process_cmd(struct command *cmd)
 {
-	return !cmd->error_string && !cmd->skip_update;
+	return !cmd->report.error_message && !cmd->skip_update;
 }
 
 static void warn_if_skipped_connectivity_check(struct command *commands,
@@ -1414,24 +1679,24 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
 		if (!transaction) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "transaction failed to start";
+			cmd->report.error_message = "transaction failed to start";
 			continue;
 		}
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (!cmd->error_string
+		if (!cmd->report.error_message
 		    && ref_transaction_commit(transaction, &err)) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "failed to update ref";
+			cmd->report.error_message = "failed to update ref";
 		}
 		ref_transaction_free(transaction);
 	}
@@ -1454,12 +1719,12 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			goto failure;
 	}
 
@@ -1472,8 +1737,8 @@ static void execute_commands_atomic(struct command *commands,
 
 failure:
 	for (cmd = commands; cmd; cmd = cmd->next)
-		if (!cmd->error_string)
-			cmd->error_string = reported_error;
+		if (!cmd->report.error_message)
+			cmd->report.error_message = reported_error;
 
 cleanup:
 	ref_transaction_free(transaction);
@@ -1490,10 +1755,11 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
-			cmd->error_string = "unpacker error";
+			cmd->report.error_message = "unpacker error";
 		return;
 	}
 
@@ -1519,10 +1785,25 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "pre-receive hook declined";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "pre-receive hook declined";
 		}
 		return;
 	}
@@ -1533,8 +1814,8 @@ static void execute_commands(struct command *commands,
 	 */
 	if (tmp_objdir_migrate(tmp_objdir) < 0) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "unable to migrate objects to permanent storage";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "unable to migrate objects to permanent storage";
 		}
 		return;
 	}
@@ -1545,6 +1826,14 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->report.error_message &&
+			    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED) &&
+			    (cmd->run_proc_receive || use_atomic))
+				cmd->report.error_message = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -1906,7 +2195,7 @@ static void update_shallow_info(struct command *commands,
 		if (is_null_oid(&cmd->new_oid))
 			continue;
 		if (ref_status[cmd->index]) {
-			cmd->error_string = "shallow update not allowed";
+			cmd->report.error_message = "shallow update not allowed";
 			cmd->skip_update = 1;
 		}
 	}
@@ -1921,12 +2210,12 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			packet_buf_write(&buf, "ok %s\n",
 					 cmd->ref_name);
 		else
 			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+					 cmd->ref_name, cmd->report.error_message);
 	}
 	packet_buf_flush(&buf);
 
@@ -2030,7 +2319,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		if (!check_cert_push_options(&push_options)) {
 			struct command *cmd;
 			for (cmd = commands; cmd; cmd = cmd->next)
-				cmd->error_string = "inconsistent push options";
+				cmd->report.error_message = "inconsistent push options";
 		}
 
 		prepare_shallow_info(&si, &shallow);
diff --git a/remote.h b/remote.h
index 11d8719b58..6dbc7e0559 100644
--- a/remote.h
+++ b/remote.h
@@ -93,6 +93,20 @@ int for_each_remote(each_remote_fn fn, void *priv);
 
 int remote_has_url(struct remote *remote, const char *url);
 
+struct ref_push_report_options {
+	const char *ref_name;
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	unsigned int forced_update:1;
+	struct ref_push_report_options *next;
+};
+
+struct ref_push_report {
+	const char *message;
+	const char *error_message;
+	struct ref_push_report_options *options;
+};
+
 struct ref {
 	struct ref *next;
 	struct object_id old_oid;
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..2d619aeaad
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,174 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int die_version = 0;
+static int die_readline = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static int verbose = 0;
+static int version = 1;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1 || die_version)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ' ||
+		    die_readline)
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_BOOL(0, "die-version", &die_version,
+			 "die during version negotiation"),
+		OPT_BOOL(0, "die-readline", &die_readline,
+			 "die when readline"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 590b2efca7..a0d3966b29 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -46,6 +46,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ddc8e990e9..07034d3f38 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -35,6 +35,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
new file mode 100644
index 0000000000..bb6ec92a92
--- /dev/null
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -0,0 +1,64 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
new file mode 100644
index 0000000000..4814f74dc2
--- /dev/null
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -0,0 +1,66 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
new file mode 100644
index 0000000000..c5fe4cb37b
--- /dev/null
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -0,0 +1,217 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: bad protocol version: 1
+	remote: error: proc-receive version "0" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
new file mode 100644
index 0000000000..53b47b0185
--- /dev/null
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -0,0 +1,160 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
new file mode 100644
index 0000000000..f726b7ca9c
--- /dev/null
+++ b/t/t5411/test-0020-report-ng.sh
@@ -0,0 +1,67 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
new file mode 100644
index 0000000000..fbf5569103
--- /dev/null
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -0,0 +1,69 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
new file mode 100644
index 0000000000..92a415b929
--- /dev/null
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
new file mode 100644
index 0000000000..acbf93e40a
--- /dev/null
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -0,0 +1,46 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
new file mode 100644
index 0000000000..c3946f329a
--- /dev/null
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
new file mode 100644
index 0000000000..d093b1a579
--- /dev/null
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
new file mode 100644
index 0000000000..d0c4da8b23
--- /dev/null
+++ b/t/t5411/test-0026-push-options.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
new file mode 100644
index 0000000000..c89a1e7c57
--- /dev/null
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -0,0 +1,82 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push \
+		--porcelain \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
new file mode 100644
index 0000000000..44c99d3831
--- /dev/null
+++ b/t/t5411/test-0030-report-ok.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
new file mode 100644
index 0000000000..3223b26184
--- /dev/null
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -0,0 +1,36 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
new file mode 100644
index 0000000000..a743aa8018
--- /dev/null
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -0,0 +1,256 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
new file mode 100644
index 0000000000..439b97b06e
--- /dev/null
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -0,0 +1,265 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/for/a/b/c/topic    [new reference]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
new file mode 100644
index 0000000000..aca2b0676c
--- /dev/null
+++ b/t/t5411/test-0034-report-ft.sh
@@ -0,0 +1,44 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
new file mode 100644
index 0000000000..30ffffb352
--- /dev/null
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (fall-through, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    <COMMIT-B>:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
new file mode 100644
index 0000000000..12acf9ea1f
--- /dev/null
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -0,0 +1,159 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
new file mode 100644
index 0000000000..f4a2c56e14
--- /dev/null
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -0,0 +1,162 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
new file mode 100644
index 0000000000..1dd6a97a97
--- /dev/null
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -0,0 +1,89 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic2
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
+	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
new file mode 100644
index 0000000000..32ebf63dcf
--- /dev/null
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -0,0 +1,91 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/bar    [new branch]
+	*    HEAD:refs/heads/baz    [new branch]
+	*    HEAD:refs/for/next/topic2    [new reference]
+	*    HEAD:refs/heads/foo    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
+	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 04/11] New capability "report-status-v2" for git-push
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (3 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 03/11] receive-pack: add new " Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 05/11] doc: add document for capability report-status-v2 Jiang Xin
                                                         ` (6 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The new introduced "proc-receive" hook may handle a command for a
pseudo-reference with a zero-old as its old-oid, while the hook may
create or update a reference with different name, different new-oid,
and different old-oid (the reference may exist already with a non-zero
old-oid).  Current "report-status" protocol cannot report the status for
such reference rewrite.

Add new capability "report-status-v2" and new report protocol which is
not backward compatible for report of git-push.

If a user pushes to a pseudo-reference "refs/for/master/topic", and
"receive-pack" creates two new references "refs/changes/23/123/1" and
"refs/changes/24/124/1", for client without the knowledge of
"report-status-v2", "receive-pack" will only send "ok/ng" directives in
the report, such as:

    ok ref/for/master/topic

But for client which has the knowledge of "report-status-v2",
"receive-pack" will use "option" directives to report more attributes
for the reference given by the above "ok/ng" directive.

    ok refs/for/master/topic
    option refname refs/changes/23/123/1
    option new-oid <new-oid>
    ok refs/for/master/topic
    option refname refs/changes/24/124/1
    option new-oid <new-oid>

The client will report two new created references to the end user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        |  52 +++++++-
 builtin/send-pack.c                           |  21 +++-
 remote.c                                      |   4 +-
 remote.h                                      |   2 +-
 send-pack.c                                   |  99 ++++++++++++---
 t/t5411-proc-receive-hook.sh                  |   9 ++
 t/t5411/once-0010-report-status-v1.sh         |  83 ++++++++++++
 t/t5411/test-0032-report-with-options.sh      |  14 +--
 ...est-0033-report-with-options--porcelain.sh |  14 +--
 ...t-0036-report-multi-rewrite-for-one-ref.sh |  11 +-
 ...rt-multi-rewrite-for-one-ref--porcelain.sh |  11 +-
 t/t5411/test-0038-report-mixed-refs.sh        |   2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |   2 +-
 transport-helper.c                            |  53 +++++++-
 transport.c                                   | 118 +++++++++++++-----
 15 files changed, 418 insertions(+), 77 deletions(-)
 create mode 100644 t/t5411/once-0010-report-status-v1.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 717bebd3e1..5544d03718 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -57,6 +57,7 @@ static int advertise_push_options;
 static int unpack_limit = 100;
 static off_t max_input_size;
 static int report_status;
+static int report_status_v2;
 static int use_sideband;
 static int use_atomic;
 static int use_push_options;
@@ -240,7 +241,7 @@ static void show_ref(const char *path, const struct object_id *oid)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status report-status-v2 delete-refs side-band-64k quiet");
 		if (advertise_atomic_push)
 			strbuf_addstr(&cap, " atomic");
 		if (prefer_ofs_delta)
@@ -1915,6 +1916,8 @@ static struct command *read_head_info(struct packet_reader *reader,
 			const char *feature_list = reader->line + linelen + 1;
 			if (parse_feature_request(feature_list, "report-status"))
 				report_status = 1;
+			if (parse_feature_request(feature_list, "report-status-v2"))
+				report_status_v2 = 1;
 			if (parse_feature_request(feature_list, "side-band-64k"))
 				use_sideband = LARGE_PACKET_MAX;
 			if (parse_feature_request(feature_list, "quiet"))
@@ -2226,6 +2229,49 @@ static void report(struct command *commands, const char *unpack_status)
 	strbuf_release(&buf);
 }
 
+static void report_v2(struct command *commands, const char *unpack_status)
+{
+	struct command *cmd;
+	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
+
+	packet_buf_write(&buf, "unpack %s\n",
+			 unpack_status ? unpack_status : "ok");
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		int count = 0;
+
+		if (!cmd->report.error_message)
+			packet_buf_write(&buf, "ok %s\n",
+					 cmd->ref_name);
+		else
+			packet_buf_write(&buf, "ng %s %s\n",
+					 cmd->ref_name,
+					 cmd->report.error_message);
+		for (options = cmd->report.options; options; options = options->next) {
+			if (count++ > 0)
+				packet_buf_write(&buf, "ok %s\n", cmd->ref_name);
+			if (options->ref_name)
+				packet_buf_write(&buf, "option refname %s\n",
+						 options->ref_name);
+			if (options->old_oid)
+				packet_buf_write(&buf, "option old-oid %s\n",
+						 oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				packet_buf_write(&buf, "option new-oid %s\n",
+						 oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				packet_buf_write(&buf, "option forced-update\n");
+		}
+	}
+	packet_buf_flush(&buf);
+
+	if (use_sideband)
+		send_sideband(1, 1, buf.buf, buf.len, use_sideband);
+	else
+		write_or_die(1, buf.buf, buf.len);
+	strbuf_release(&buf);
+}
+
 static int delete_only(struct command *commands)
 {
 	struct command *cmd;
@@ -2334,7 +2380,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
-		if (report_status)
+		if (report_status_v2)
+			report_v2(commands, unpack_status);
+		else if (report_status)
 			report(commands, unpack_status);
 		run_receive_hook(commands, "post-receive", 1,
 				 &push_options);
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 2b9610f121..9d0df751d6 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -29,10 +29,12 @@ static struct send_pack_args args;
 static void print_helper_status(struct ref *ref)
 {
 	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
 
 	for (; ref; ref = ref->next) {
 		const char *msg = NULL;
 		const char *res;
+		int count = 0;
 
 		switch(ref->status) {
 		case REF_STATUS_NONE:
@@ -86,14 +88,29 @@ static void print_helper_status(struct ref *ref)
 
 		strbuf_reset(&buf);
 		strbuf_addf(&buf, "%s %s", res, ref->name);
-		if (ref->remote_status)
-			msg = ref->remote_status;
+		if (ref->report.error_message)
+			msg = ref->report.error_message;
 		if (msg) {
 			strbuf_addch(&buf, ' ');
 			quote_two_c_style(&buf, "", msg, 0);
 		}
 		strbuf_addch(&buf, '\n');
 
+		for (options = ref->report.options; options; options = options->next) {
+			if (count++ > 0)
+				strbuf_addf(&buf, "ok %s\n", ref->name);
+			if (options->ref_name)
+				strbuf_addf(&buf, "option refname %s\n",
+					    options->ref_name);
+			if (options->old_oid)
+				strbuf_addf(&buf, "option old-oid %s\n",
+					    oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				strbuf_addf(&buf, "option new-oid %s\n",
+					    oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				strbuf_addstr(&buf, "option forced-update\n");
+		}
 		write_or_die(1, buf.buf, buf.len);
 	}
 	strbuf_release(&buf);
diff --git a/remote.c b/remote.c
index 534c6426f1..cc4d542fd3 100644
--- a/remote.c
+++ b/remote.c
@@ -789,7 +789,7 @@ struct ref *copy_ref(const struct ref *ref)
 	memcpy(cpy, ref, len);
 	cpy->next = NULL;
 	cpy->symref = xstrdup_or_null(ref->symref);
-	cpy->remote_status = xstrdup_or_null(ref->remote_status);
+	cpy->report.error_message = xstrdup_or_null(ref->report.error_message);
 	cpy->peer_ref = copy_ref(ref->peer_ref);
 	return cpy;
 }
@@ -811,7 +811,7 @@ void free_one_ref(struct ref *ref)
 	if (!ref)
 		return;
 	free_one_ref(ref->peer_ref);
-	free(ref->remote_status);
+	free((void *)ref->report.error_message);
 	free(ref->symref);
 	free(ref);
 }
diff --git a/remote.h b/remote.h
index 6dbc7e0559..4e9a17de3c 100644
--- a/remote.h
+++ b/remote.h
@@ -153,7 +153,7 @@ struct ref {
 		REF_STATUS_EXPECTING_REPORT,
 		REF_STATUS_ATOMIC_PUSH_FAILED
 	} status;
-	char *remote_status;
+	struct ref_push_report report;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
 };
diff --git a/send-pack.c b/send-pack.c
index 0abee22283..d8a6db47c7 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -154,25 +154,79 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 {
 	struct ref *hint;
 	int ret;
+	int new_options = 1;
 
 	hint = NULL;
 	ret = receive_unpack_status(reader);
 	while (1) {
+		struct object_id old_oid, new_oid;
+		const char *head;
 		const char *refname;
-		char *msg;
+		char *p;
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
-		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
-			error("invalid ref status from remote: %s", reader->line);
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			error("invalid status line from remote: %s", reader->line);
 			ret = -1;
 			break;
 		}
+		*p++ = '\0';
+
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					error("'option' without a matching 'ok/ng' directive");
+					new_options = 0;
+				}
+				ret = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			continue;
+		}
 
-		refname = reader->line + 3;
-		msg = strchr(refname, ' ');
-		if (msg)
-			*msg++ = '\0';
-
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			error("invalid ref status from remote: %s", head);
+			ret = -1;
+			break;
+		}
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
 			hint = find_ref_by_name(hint, refname);
@@ -183,19 +237,24 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 					refname);
 			continue;
 		}
-		if (hint->status != REF_STATUS_EXPECTING_REPORT) {
+		if (hint->status != REF_STATUS_EXPECTING_REPORT &&
+		    hint->status != REF_STATUS_OK &&
+		    hint->status != REF_STATUS_REMOTE_REJECT) {
 			warning("remote reported status on unexpected ref: %s",
 					refname);
 			continue;
 		}
-
-		if (reader->line[0] == 'o' && reader->line[1] == 'k')
-			hint->status = REF_STATUS_OK;
-		else
+		if (!strcmp(head, "ng")) {
 			hint->status = REF_STATUS_REMOTE_REJECT;
-		hint->remote_status = xstrdup_or_null(msg);
-		/* start our next search from the next ref */
-		hint = hint->next;
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+		} else {
+			hint->status = REF_STATUS_OK;
+			hint->report.message = xstrdup_or_null(p);
+		}
+		new_options = 1;
 	}
 	return ret;
 }
@@ -370,7 +429,9 @@ int send_pack(struct send_pack_args *args,
 	struct packet_reader reader;
 
 	/* Does the other end support the reporting? */
-	if (server_supports("report-status"))
+	if (server_supports("report-status-v2"))
+		status_report = 2;
+	else if (server_supports("report-status"))
 		status_report = 1;
 	if (server_supports("delete-refs"))
 		allow_deleting_refs = 1;
@@ -419,8 +480,10 @@ int send_pack(struct send_pack_args *args,
 
 	use_push_options = push_options_supported && args->push_options;
 
-	if (status_report)
+	if (status_report == 1)
 		strbuf_addstr(&cap_buf, " report-status");
+	else if (status_report == 2)
+		strbuf_addstr(&cap_buf, " report-status-v2");
 	if (use_sideband)
 		strbuf_addstr(&cap_buf, " side-band-64k");
 	if (quiet_supported && (args->quiet || !args->progress))
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 3a684353a8..746487286f 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -78,6 +78,15 @@ run_proc_receive_hook_test() {
 # Initialize the upstream repository and local workbench.
 setup_upstream_and_workbench
 
+# Load test cases that only need to be executed once.
+for t in  "$TEST_DIRECTORY"/t5411/once-*.sh
+do
+	. "$t"
+done
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
 # Run test cases for 'proc-receive' hook on local file protocol.
 run_proc_receive_hook_test local
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
new file mode 100644
index 0000000000..f93d760cfd
--- /dev/null
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -0,0 +1,83 @@
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic1" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/23/123/1" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/24/124/2" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update" \
+		-r "ng refs/for/next/topic target branch not exist"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic1(A)  foo(A)  refs/for/next/topic(A)  refs/for/master/topic2(A)
+test_expect_success "proc-receive: report status v1" '
+	{
+		printf "%s %s refs/heads/master\0report-status\n" \
+			$A $B | packetize &&
+		printf "%s %s refs/for/master/topic1\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/heads/foo\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/next/topic\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/master/topic2\n" \
+			$ZERO_OID $A | packetize &&
+		printf 0000 &&
+		printf "" | git -C "$upstream" pack-objects --stdout
+	} | git receive-pack "$upstream" --stateless-rpc \
+	>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	# pre-receive hook
+	pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	# proc-receive hook
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	proc-receive> ok refs/for/master/topic1
+	proc-receive> option fall-through
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/23/123/1
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/24/124/2
+	proc-receive> option old-oid <COMMIT-B>
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> option forced-update
+	proc-receive> ng refs/for/next/topic target branch not exist
+	000eunpack ok
+	0019ok refs/heads/master
+	001eok refs/for/master/topic1
+	0016ok refs/heads/foo
+	0033ng refs/for/next/topic target branch not exist
+	001eok refs/for/master/topic2
+	0000# post-receive hook
+	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/for/master/topic1
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a743aa8018..3742ed3c3d 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -56,7 +56,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -89,7 +89,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -123,7 +123,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -155,7 +155,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -189,7 +189,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -241,9 +241,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 439b97b06e..f18ba9f06f 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -57,7 +57,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -92,7 +92,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -127,7 +127,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -160,7 +160,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -195,7 +195,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual
@@ -249,9 +249,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
-	*    HEAD:refs/for/master/topic    [new reference]
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 12acf9ea1f..64662afdc6 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -147,7 +151,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index f4a2c56e14..a7e29af0c2 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/23/123/1    [new reference]
+	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 1dd6a97a97..54bec4c026 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic2
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 32ebf63dcf..605b3cb5c9 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	*    HEAD:refs/heads/baz    [new branch]
 	*    HEAD:refs/for/next/topic2    [new reference]
 	*    HEAD:refs/heads/foo    [new branch]
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
 	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
 	Done
diff --git a/transport-helper.c b/transport-helper.c
index a46afcb69d..21d6e47966 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -725,6 +725,49 @@ static int push_update_ref_status(struct strbuf *buf,
 {
 	char *refname, *msg;
 	int status, forced = 0;
+	static struct ref *hint = NULL;
+	static int new_options = 1;
+
+	if (starts_with(buf->buf, "option ")) {
+		struct ref_push_report_options *options;
+		struct object_id old_oid, new_oid;
+		const char *key, *val;
+		char *p;
+
+		if (!hint)
+			die(_("'option' without a matching 'ok/error' directive"));
+		options = hint->report.options;
+		while (options && options->next)
+			options = options->next;
+		if (new_options) {
+			if (!options) {
+				hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = hint->report.options;
+			} else {
+				options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = options->next;
+			}
+			new_options = 0;
+		}
+		assert(options);
+		key = buf->buf + 7;
+		p = strchr(key, ' ');
+		if (p)
+			*p++ = '\0';
+		val = p;
+		if (!strcmp(key, "refname"))
+			options->ref_name = xstrdup_or_null(val);
+		else if (!strcmp(key, "old-oid") && val &&
+			 !parse_oid_hex(val, &old_oid, &val))
+			options->old_oid = oiddup(&old_oid);
+		else if (!strcmp(key, "new-oid") && val &&
+			 !parse_oid_hex(val, &new_oid, &val))
+			options->new_oid = oiddup(&new_oid);
+		else if (!strcmp(key, "forced-update"))
+			options->forced_update = 1;
+		/* Not update remote namespace again. */
+		return 1;
+	}
 
 	if (starts_with(buf->buf, "ok ")) {
 		status = REF_STATUS_OK;
@@ -787,8 +830,11 @@ static int push_update_ref_status(struct strbuf *buf,
 		*ref = find_ref_by_name(remote_refs, refname);
 	if (!*ref) {
 		warning(_("helper reported unexpected status of %s"), refname);
+		hint = NULL;
 		return 1;
 	}
+	hint = *ref;
+	new_options = 1;
 
 	if ((*ref)->status != REF_STATUS_NONE) {
 		/*
@@ -801,7 +847,12 @@ static int push_update_ref_status(struct strbuf *buf,
 
 	(*ref)->status = status;
 	(*ref)->forced_update |= forced;
-	(*ref)->remote_status = msg;
+	if (msg) {
+		if (status == REF_STATUS_OK)
+			(*ref)->report.message = msg;
+		else
+			(*ref)->report.error_message = msg;
+	}
 	return !(status == REF_STATUS_OK);
 }
 
diff --git a/transport.c b/transport.c
index 26fefd13c1..5baa30dd74 100644
--- a/transport.c
+++ b/transport.c
@@ -461,13 +461,21 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
+			     struct ref_push_report_options *options,
 			     int porcelain, int summary_width)
 {
+	const char *to_name;
+
+	if (options && options->ref_name)
+		to_name = options->ref_name;
+	else
+		to_name = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +489,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -493,27 +503,52 @@ static void print_ref_status(char flag, const char *summary,
 	}
 }
 
-static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
+static void print_ok_ref_status(struct ref *ref,
+				struct ref_push_report_options *options,
+				int porcelain, int summary_width)
 {
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	const char *ref_name;
+	int forced_update;
+
+	if (options && options->old_oid)
+		old_oid = options->old_oid;
+	else
+		old_oid = &ref->old_oid;
+	if (options && options->new_oid)
+		new_oid = options->new_oid;
+	else
+		new_oid = &ref->new_oid;
+	if (options && options->forced_update)
+		forced_update = options->forced_update;
+	else
+		forced_update = ref->forced_update;
+	if (options && options->ref_name)
+		ref_name = options->ref_name;
+	else
+		ref_name = ref->name;
+
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
-				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+				 options, porcelain, summary_width);
+	else if (is_null_oid(old_oid))
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(ref_name, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(ref_name, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 ref, ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
 
-		strbuf_add_unique_abbrev(&quickref, &ref->old_oid,
+		strbuf_add_unique_abbrev(&quickref, old_oid,
 					 DEFAULT_ABBREV);
-		if (ref->forced_update) {
+		if (forced_update) {
 			strbuf_addstr(&quickref, "...");
 			type = '+';
 			msg = "forced update";
@@ -522,17 +557,18 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 			type = ' ';
 			msg = NULL;
 		}
-		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
+		strbuf_add_unique_abbrev(&quickref, new_oid,
 					 DEFAULT_ABBREV);
 
 		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
-				 int porcelain, int summary_width)
+static int _print_one_push_status(struct ref *ref, const char *dest, int count,
+				  struct ref_push_report_options *options,
+				  int porcelain, int summary_width)
 {
 	if (!count) {
 		char *url = transport_anonymize_url(dest);
@@ -543,65 +579,89 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	switch(ref->status) {
 	case REF_STATUS_NONE:
 		print_ref_status('X', "[no match]", ref, NULL, NULL,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
 		print_ref_status('!', "[rejected]", ref, NULL,
 				 "remote does not support deleting refs",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
 		print_ref_status('=', "[up to date]", ref,
-				 ref->peer_ref, NULL, porcelain, summary_width);
+				 ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "non-fast-forward", porcelain, summary_width);
+				 "non-fast-forward",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "already exists", porcelain, summary_width);
+				 "already exists",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "fetch first", porcelain, summary_width);
+				 "fetch first",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "needs force", porcelain, summary_width);
+				 "needs force",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "stale info", porcelain, summary_width);
+				 "stale info",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
 				 "new shallow roots not allowed",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
 		print_ref_status('!', "[remote rejected]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
-				 ref->remote_status, porcelain, summary_width);
+				 ref->report.error_message,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
 		print_ref_status('!', "[remote failure]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "atomic push failed", porcelain, summary_width);
+				 "atomic push failed",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-		print_ok_ref_status(ref, porcelain, summary_width);
+		print_ok_ref_status(ref, options, porcelain, summary_width);
 		break;
 	}
 
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	struct ref_push_report_options *options;
+	int n = 0;
+
+	if (!ref->report.options)
+		return _print_one_push_status(ref, dest, count,
+					      NULL, porcelain, summary_width);
+
+	for (options = ref->report.options; options; options = options->next)
+		_print_one_push_status(ref, dest, count + n++,
+				       options, porcelain, summary_width);
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 05/11] doc: add document for capability report-status-v2
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (4 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 04/11] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 06/11] receive-pack: feed report options to post-receive Jiang Xin
                                                         ` (5 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add ABNF notation for capability 'report-status-v2' which extends
capability 'report-status' by adding additional option lines.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/technical/pack-protocol.txt     | 43 +++++++++++++++++--
 .../technical/protocol-capabilities.txt       | 17 ++++++--
 2 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt
index d5ce4eea8a..7547b9ca89 100644
--- a/Documentation/technical/pack-protocol.txt
+++ b/Documentation/technical/pack-protocol.txt
@@ -503,8 +503,8 @@ The reference discovery phase is done nearly the same way as it is in the
 fetching protocol. Each reference obj-id and name on the server is sent
 in packet-line format to the client, followed by a flush-pkt.  The only
 real difference is that the capability listing is different - the only
-possible values are 'report-status', 'delete-refs', 'ofs-delta' and
-'push-options'.
+possible values are 'report-status', 'report-status-v2', 'delete-refs',
+'ofs-delta', 'atomic' and 'push-options'.
 
 Reference Update Request and Packfile Transfer
 ----------------------------------------------
@@ -625,7 +625,7 @@ Report Status
 -------------
 
 After receiving the pack data from the sender, the receiver sends a
-report if 'report-status' capability is in effect.
+report if 'report-status' or 'report-status-v2' capability is in effect.
 It is a short listing of what happened in that update.  It will first
 list the status of the packfile unpacking as either 'unpack ok' or
 'unpack [error]'.  Then it will list the status for each of the references
@@ -647,6 +647,43 @@ update was successful, or 'ng [refname] [error]' if the update was not.
   error-msg         = 1*(OCTET) ; where not "ok"
 ----
 
+New capability "report-status-v2" extends the protocol by adding new option
+lines in order to support reporting of reference rewritten by the
+"proc-receive" hook.  The "proc-receive" hook may handle a command for a
+pseudo-reference which may create or update one or more references, and each
+reference may have different name, different new-oid, and different old-oid.
+
+----
+  report-status-v2  = unpack-status
+		      1*(command-status-v2)
+		      flush-pkt
+
+  unpack-status     = PKT-LINE("unpack" SP unpack-result)
+  unpack-result     = "ok" / error-msg
+
+  command-status-v2 = command-ok-v2 / command-fail-v2
+  command-ok-v2     = command-ok
+		      *option-line
+  command-fail-v2   = command-fail
+		      *option-line
+
+  command-ok        = PKT-LINE("ok" SP refname)
+  command-fail      = PKT-LINE("ng" SP refname SP error-msg)
+
+  error-msg         = 1*(OCTET) ; where not "ok"
+
+  option-line       = *1(option-refname)
+		      *1(option-old-oid)
+		      *1(option-new-oid)
+		      *1(option-forced-update)
+
+  option-refname    = PKT-LINE("option" SP "refname" SP refname)
+  option-old-oid    = PKT-LINE("option" SP "old-oid" SP obj-id)
+  option-new-oid    = PKT-LINE("option" SP "new-oid" SP obj-id)
+  option-force      = PKT-LINE("option" SP "forced-update")
+
+----
+
 Updates can be unsuccessful for a number of reasons.  The reference can have
 changed since the reference discovery phase was originally sent, meaning
 someone pushed in the meantime.  The reference being pushed could be a
diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 2b267c0da6..91fd976094 100644
--- a/Documentation/technical/protocol-capabilities.txt
+++ b/Documentation/technical/protocol-capabilities.txt
@@ -22,9 +22,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 'atomic', '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', 'report-status-v2', '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
@@ -269,6 +269,17 @@ each reference was updated successfully.  If any of those were not
 successful, it will send back an error message.  See pack-protocol.txt
 for example messages.
 
+report-status-v2
+----------------
+
+Capability 'report-status-v2' extends capability 'report-status' by
+adding new option lines in order to support reference rewritten by
+the"proc-receive" hook.  The "proc-receive" hook may handle a command
+for a pseudo-reference which may create or update a reference with
+different name, new-oid, and old-oid.  While the capability
+"report-status" cannot report for such case.  See pack-protocol.txt
+for details.
+
 delete-refs
 -----------
 
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 06/11] receive-pack: feed report options to post-receive
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (5 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 05/11] doc: add document for capability report-status-v2 Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 07/11] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
                                                         ` (4 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When commands are fed to the "post-receive" hook, report options will
be parsed and the real old-oid, new-oid, reference name will feed to
the "post-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        | 29 ++++++++++++++++---
 t/t5411/once-0010-report-status-v1.sh         |  3 +-
 t/t5411/test-0032-report-with-options.sh      | 14 ++++-----
 ...est-0033-report-with-options--porcelain.sh | 14 ++++-----
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 11 +++++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 11 +++++--
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 8 files changed, 59 insertions(+), 27 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 5544d03718..e2478f34b8 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -776,17 +776,38 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 {
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
+	static struct ref_push_report_options *options = NULL;
 
 	while (cmd &&
 	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
+	if (!bufp)
+		return 0; /* OK, can feed something. */
 	strbuf_reset(&state->buf);
-	strbuf_addf(&state->buf, "%s %s %s\n",
-		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
-		    cmd->ref_name);
-	state->cmd = cmd->next;
+	if (!options)
+		options = cmd->report.options;
+	if (options) {
+		struct object_id *old_oid;
+		struct object_id *new_oid;
+		const char *ref_name;
+
+		old_oid = options->old_oid ? options->old_oid : &cmd->old_oid;
+		new_oid = options->new_oid ? options->new_oid : &cmd->new_oid;
+		ref_name = options->ref_name ? options->ref_name : cmd->ref_name;
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(old_oid), oid_to_hex(new_oid),
+			    ref_name);
+		options = options->next;
+		if (!options)
+			state->cmd = cmd->next;
+	} else {
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
+			    cmd->ref_name);
+		state->cmd = cmd->next;
+	}
 	if (bufp) {
 		*bufp = state->buf.buf;
 		*sizep = state->buf.len;
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index f93d760cfd..4385628d06 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -68,7 +68,8 @@ test_expect_success "proc-receive: report status v1" '
 	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
 	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/changes/23/123/1
+	post-receive< <COMMIT-B> <COMMIT-A> refs/for/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index 3742ed3c3d..b77b78c49f 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -54,7 +54,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -87,7 +87,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -121,7 +121,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
@@ -153,7 +153,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
@@ -187,7 +187,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
@@ -237,9 +237,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index f18ba9f06f..1fe352b686 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -55,7 +55,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -90,7 +90,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -125,7 +125,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
@@ -158,7 +158,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
@@ -193,7 +193,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
@@ -245,9 +245,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 64662afdc6..27d58edd14 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 * [new reference] HEAD -> refs/changes/24/124/1
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/24/124/1
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/23/123/1
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index a7e29af0c2..77b5b22ed4 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	*    HEAD:refs/changes/24/124/1    [new reference]
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/24/124/1    [new reference]
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
@@ -151,7 +155,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/23/123/1    [new reference]
 	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 54bec4c026..a74a2cb449 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 605b3cb5c9..e4baa13ea3 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
 	*    HEAD:refs/heads/bar    [new branch]
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 07/11] refs.c: refactor to reuse ref_is_hidden()
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (6 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 06/11] receive-pack: feed report options to post-receive Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 08/11] receive-pack: new config receive.procReceiveRefs Jiang Xin
                                                         ` (3 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new function `ref_is_matched()` to reuse `ref_is_hidden()`. Will use
this function for `receive-pack` to check commands with specific
prefixes.

Test case t5512 covered this change.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 refs.c | 13 +++++++++----
 refs.h |  1 +
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 224ff66c7b..76a91ac0f6 100644
--- a/refs.c
+++ b/refs.c
@@ -1344,14 +1344,14 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 	return 0;
 }
 
-int ref_is_hidden(const char *refname, const char *refname_full)
+int ref_matches(struct string_list *match_refs, const char *refname, const char *refname_full)
 {
 	int i;
 
-	if (!hide_refs)
+	if (!match_refs)
 		return 0;
-	for (i = hide_refs->nr - 1; i >= 0; i--) {
-		const char *match = hide_refs->items[i].string;
+	for (i = match_refs->nr - 1; i >= 0; i--) {
+		const char *match = match_refs->items[i].string;
 		const char *subject;
 		int neg = 0;
 		const char *p;
@@ -1377,6 +1377,11 @@ int ref_is_hidden(const char *refname, const char *refname_full)
 	return 0;
 }
 
+int ref_is_hidden(const char *refname, const char *refname_full)
+{
+	return ref_matches(hide_refs, refname, refname_full);
+}
+
 const char *find_descendant_ref(const char *dirname,
 				const struct string_list *extras,
 				const struct string_list *skip)
diff --git a/refs.h b/refs.h
index a92d2c74c8..9907dd12b0 100644
--- a/refs.h
+++ b/refs.h
@@ -727,6 +727,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
  * parameter always points to the full ref name.
  */
 int ref_is_hidden(const char *, const char *);
+int ref_matches(struct string_list *, const char *, const char *);
 
 enum ref_type {
 	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 08/11] receive-pack: new config receive.procReceiveRefs
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (7 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 07/11] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 09/11] t5411: test updates of remote-tracking branches Jiang Xin
                                                         ` (2 subsequent siblings)
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings match the reference names of the commands
which are sent from git client to `receive-pack`, these commands will be
executed by an external hook (named "proc-receive"), instead of the
internal `execute_commands` function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt              |  14 +++
 builtin/receive-pack.c                        |  43 +++++--
 t/t5411/once-0010-report-status-v1.sh         |   4 +
 t/t5411/test-0010-proc-receive-settings.sh    |   7 ++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++++++++++++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 ++++++++++++++++++
 6 files changed, 288 insertions(+), 7 deletions(-)
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..e7b967feeb 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,20 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
+
+	For example, if this variable is set to "refs/for", pushing to
+	reference such as "refs/for/master" will not create or update a
+	reference named "refs/for/master", but may create or update a
+	pull request directly by running the hook "proc-receive".
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e2478f34b8..136ff85688 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -78,6 +78,7 @@ static struct object_id push_cert_oid;
 static struct signature_check sigcheck;
 static const char *push_cert_nonce;
 static const char *cert_nonce_seed;
+static struct string_list proc_receive_refs;
 
 static const char *NONCE_UNSOLICITED = "UNSOLICITED";
 static const char *NONCE_BAD = "BAD";
@@ -230,6 +231,20 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		char *prefix;
+		int len;
+
+		if (!value)
+			return config_error_nonbool(var);
+		prefix = xstrdup(value);
+		len = strlen(prefix);
+		while (len && prefix[len - 1] == '/')
+			prefix[--len] = '\0';
+		string_list_append(&proc_receive_refs, prefix);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -1811,15 +1826,26 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_refs.nr > 0) {
+		struct strbuf refname_full = STRBUF_INIT;
+		size_t prefix_len;
+
+		strbuf_addstr(&refname_full, get_git_namespace());
+		prefix_len = refname_full.len;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
+
+			strbuf_setlen(&refname_full, prefix_len);
+			strbuf_addstr(&refname_full, cmd->ref_name);
+			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
+
+		strbuf_release(&refname_full);
 	}
 
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
@@ -2320,6 +2346,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		OPT_END()
 	};
 
+	string_list_init(&proc_receive_refs, 0);
+
 	packet_trace_identity("receive-pack");
 
 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
@@ -2437,5 +2465,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 	oid_array_clear(&shallow);
 	oid_array_clear(&ref);
 	free((void *)push_cert_nonce);
+	string_list_clear(&proc_receive_refs, 0);
 	return 0;
 }
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index 4385628d06..cdafc8971f 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -1,3 +1,7 @@
+test_expect_success "setup receive.procReceiveRefs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/for
+'
+
 test_expect_success "setup proc-receive hook" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
diff --git a/t/t5411/test-0010-proc-receive-settings.sh b/t/t5411/test-0010-proc-receive-settings.sh
new file mode 100644
index 0000000000..a36809927b
--- /dev/null
+++ b/t/t5411/test-0010-proc-receive-settings.sh
@@ -0,0 +1,7 @@
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
new file mode 100644
index 0000000000..b07c999f53
--- /dev/null
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -0,0 +1,113 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
new file mode 100644
index 0000000000..0dd9824616
--- /dev/null
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -0,0 +1,114 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+	-    :refs/heads/foo    [deleted]
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 09/11] t5411: test updates of remote-tracking branches
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (8 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 08/11] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 10/11] transport: parse report options for tracking refs Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 11/11] doc: add documentation for the proc-receive hook Jiang Xin
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

In order to test update of remote-tracking branches for special refs,
add new "remote.origin.fetch" settings and test cases.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 48 +++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 27d58edd14..8eec3a1ed8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -1,3 +1,18 @@
+test_expect_success "setup git config for remote-tracking of special refs" '
+	(
+		cd workbench &&
+		if ! git config --get-all remote.origin.fetch | grep refs/for/
+		then
+			git config --add remote.origin.fetch \
+				"+refs/for/*:refs/t/for/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/pull/*:refs/t/pull/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/changes/*:refs/t/changes/*"
+		fi
+	)
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -58,6 +73,17 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -120,6 +146,17 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -167,3 +204,14 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	EOF
 	test_cmp expect actual
 '
+
+test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 10/11] transport: parse report options for tracking refs
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (9 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 09/11] t5411: test updates of remote-tracking branches Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  2020-05-18  9:40                                       ` [PATCH v16 11/11] doc: add documentation for the proc-receive hook Jiang Xin
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a pseudo reference (such as "refs/for/master/topic"), may
create or update one or more references.  The real names of the
references will be stored in the report options.  Parse report options
to create or update remote-tracking branches properly.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 22 +++++++---
 transport.c                                   | 40 ++++++++++++++-----
 2 files changed, 46 insertions(+), 16 deletions(-)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 8eec3a1ed8..73283d81e8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -78,10 +78,14 @@ test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-B> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
@@ -151,10 +155,14 @@ test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-A> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
@@ -210,8 +218,10 @@ test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/23/123/1
+	<COMMIT-B> refs/t/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/2
 '
diff --git a/transport.c b/transport.c
index 5baa30dd74..cd01f82f5c 100644
--- a/transport.c
+++ b/transport.c
@@ -437,28 +437,48 @@ int transport_refs_pushed(struct ref *ref)
 	return 0;
 }
 
-void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+static void update_tracking_ref(struct remote *remote, char *refname,
+				struct object_id *new_oid, int deletion,
+				int verbose)
 {
 	struct refspec_item rs;
 
-	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
-		return;
-
-	rs.src = ref->name;
+	rs.src = refname;
 	rs.dst = NULL;
-
 	if (!remote_find_tracking(remote, &rs)) {
 		if (verbose)
 			fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst);
-		if (ref->deletion) {
+		if (deletion)
 			delete_ref(NULL, rs.dst, NULL, 0);
-		} else
-			update_ref("update by push", rs.dst, &ref->new_oid,
-				   NULL, 0, 0);
+		else
+			update_ref("update by push", rs.dst, new_oid,
+				NULL, 0, 0);
 		free(rs.dst);
 	}
 }
 
+void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+{
+	char *refname;
+	struct object_id *new_oid;
+	struct ref_push_report_options *options;
+
+	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
+		return;
+
+	options = ref->report.options;
+	if (!options)
+		update_tracking_ref(remote, ref->name, &ref->new_oid,
+				    ref->deletion, verbose);
+	else
+		for (; options; options = options->next) {
+			refname = options->ref_name ? (char *)options->ref_name : ref->name;
+			new_oid = options->new_oid ? options->new_oid : &ref->new_oid;
+			update_tracking_ref(remote, refname, new_oid,
+					    is_null_oid(new_oid), verbose);
+		}
+}
+
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     struct ref_push_report_options *options,
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v16 11/11] doc: add documentation for the proc-receive hook
  2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
                                                         ` (10 preceding siblings ...)
  2020-05-18  9:40                                       ` [PATCH v16 10/11] transport: parse report options for tracking refs Jiang Xin
@ 2020-05-18  9:40                                       ` Jiang Xin
  11 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-05-18  9:40 UTC (permalink / raw)
  To: Junio C Hamano, Jeff King, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

"git receive-pack" that accepts requests by "git push" learned to
outsource some of the ref updates to the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 62 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 81f2a87e88..d66154f606 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,68 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option fall-through)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status can be given in options
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option refname <refname>)
+    H: PKT-LINE(option old-oid <old-oid>)
+    H: PKT-LINE(option new-oid <new-oid>)
+    H: PKT-LINE(option forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook will use
+"option" directives to report extended attributes for the reference given
+by the above "ok/ng" directive.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.26.1.120.g98702cf3e9


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 00/10] New proc-receive hook for centralized workflow
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                             ` (10 more replies)
  2020-08-15 17:17                                         ` [PATCH v17 01/10] transport: not report a non-head push as a branch Jiang Xin
                                                           ` (9 subsequent siblings)
  10 siblings, 11 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

This topic introduces a new hook named "proc-receive" for
"receive-pack".  We can use this hook to implement a Gerrit-like
centralized workflow.

## Changes since v16

1. Fixes test helper for proc-receive which is not compatible with sha256.

   Run `setup_git_directory_gently()`  in `t/helper/test-proc-receive.c`,
   so that `the_repository` variable is initialized with proper hash_algo.

   See range-diff:

         3:  6ac2e0f88c !  3:  94d3053662 receive-pack: add new proc-receive hook
            @@ builtin/receive-pack.c: static void run_update_post_hook(struct command *command
             -		if (cmd->error_string || cmd->did_not_exist)
             +		if (cmd->report.error_message || cmd->did_not_exist)
              			continue;
            - 		if (!proc.args.argc)
            - 			argv_array_push(&proc.args, hook);
            + 		if (!proc.args.nr)
            + 			strvec_push(&proc.args, hook);
             @@ builtin/receive-pack.c: static void check_aliased_update_internal(struct command *cmd,
              	if (!dst_name) {
              		rp_error("refusing update to broken symref '%s'", cmd->ref_name);
            @@ t/helper/test-proc-receive.c (new)
             +
             +int cmd__proc_receive(int argc, const char **argv)
             +{
            ++	int nongit_ok = 0;
             +	struct packet_reader reader;
             +	struct command *commands = NULL;
             +	struct string_list push_options = STRING_LIST_INIT_DUP;
            @@ t/helper/test-proc-receive.c (new)
             +		OPT_END()
             +	};
             +
            ++	setup_git_directory_gently(&nongit_ok);
            ++
             +	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
             +	if (argc > 0)
             +		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
            -+
             +	packet_reader_init(&reader, 0, NULL, 0,
             +			   PACKET_READ_CHOMP_NEWLINE |
             +			   PACKET_READ_DIE_ON_ERR_PACKET);

2. Fixed broken test cases which is not compatible with sha256.

   Add capability "object-format=$GIT_DEFAULT_HASH" in
   "t/t5411/once-0010-report-status-v1.sh". See range-diff:

        4:  00c9eee210 !  4:  215d786adc New capability "report-status-v2" for git-push
           @@ builtin/receive-pack.c: static void show_ref(const char *path, const struct obje
             			strbuf_addstr(&cap, " atomic");
             		if (prefer_ofs_delta)
            @@ builtin/receive-pack.c: static struct command *read_head_info(struct packet_reader *reader,
           - 			const char *feature_list = reader->line + linelen + 1;
           + 			int len = 0;
             			if (parse_feature_request(feature_list, "report-status"))
             				report_status = 1;
            +			if (parse_feature_request(feature_list, "report-status-v2"))
           @@ t/t5411/once-0010-report-status-v1.sh (new)
            +# git push         : (B)                   refs/for/master/topic1(A)  foo(A)  refs/for/next/topic(A)  refs/for/master/topic2(A)
            +test_expect_success "proc-receive: report status v1" '
            +	{
           -+		printf "%s %s refs/heads/master\0report-status\n" \
           -+			$A $B | packetize &&
           ++		if test -z "$GIT_DEFAULT_HASH" || test "$GIT_DEFAULT_HASH" = "sha1"
           ++		then
           ++			printf "%s %s refs/heads/master\0report-status\n" \
           ++				$A $B | packetize
           ++		else
           ++			printf "%s %s refs/heads/master\0report-status object-format=$GIT_DEFAULT_HASH\n" \
           ++				$A $B | packetize
           ++		fi &&
            +		printf "%s %s refs/for/master/topic1\n" \
            +			$ZERO_OID $A | packetize &&
            +		printf "%s %s refs/heads/foo\n" \

3. Add optional modifiers for config variable "receive.procReceiveRefs".
   See range-diff:

        7:  97704cbcc6 <  -:  ---------- refs.c: refactor to reuse ref_is_hidden()
        8:  415677f838 !  7:  08b176a61a receive-pack: new config receive.procReceiveRefs
           @@ Commit message
                    git config --system --add receive.procReceiveRefs refs/for
                    git config --system --add receive.procReceiveRefs refs/drafts
            
           -    If the specific prefix strings match the reference names of the commands
           -    which are sent from git client to `receive-pack`, these commands will be
           -    executed by an external hook (named "proc-receive"), instead of the
           -    internal `execute_commands` function.
           +    If the specific prefix strings given by the config variables match the
           +    reference names of the commands which are sent from git client to
           +    `receive-pack`, these commands will be executed by an external hook
           +    (named "proc-receive"), instead of the internal `execute_commands`
           +    function.
            
                For example, if it is set to "refs/for", pushing to a reference such as
                "refs/for/master" will not create or update reference "refs/for/master",
                but may create or update a pull request directly by running the hook
                "proc-receive".
            
           +    Optional modifiers can be provided in the beginning of value of the
           +    config variable to filter commands for specific actions: create (a),
           +    mofify (m), delete (d). E.g.:
           +
           +        git config --system --add receive.procReceiveRefs ad:refs/heads
           +
                Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
            
             ## Documentation/config/receive.txt ##
           @@ Documentation/config/receive.txt: receive.hideRefs::
            +	variable is not defined, the "proc-receive" hook will never be
            +	used, and all commands will be executed by the internal
            +	`execute_commands` function.
           -+
           -+	For example, if this variable is set to "refs/for", pushing to
           -+	reference such as "refs/for/master" will not create or update a
           -+	reference named "refs/for/master", but may create or update a
           -+	pull request directly by running the hook "proc-receive".
           -+
           +++
           ++For example, if this variable is set to "refs/for", pushing to reference
           ++such as "refs/for/master" will not create or update a reference named
           ++"refs/for/master", but may create or update a pull request directly by
           ++running the hook "proc-receive".
           +++
           ++Optional modifiers can be provided in the beginning of the value to filter
           ++commands for specific actions: create (a), mofify (m), delete (d). E.g.:
           +++
           ++	git config --system --add receive.procReceiveRefs ad:refs/heads
           ++ 
             receive.updateServerInfo::
             	If set to true, git-receive-pack will run git-update-server-info
             	after receiving data from git-push and updating refs.
            
             ## builtin/receive-pack.c ##
           -@@ builtin/receive-pack.c: static struct object_id push_cert_oid;
           - static struct signature_check sigcheck;
           - static const char *push_cert_nonce;
           - static const char *cert_nonce_seed;
           -+static struct string_list proc_receive_refs;
           +@@ builtin/receive-pack.c: static int keepalive_in_sec = 5;
             
           - static const char *NONCE_UNSOLICITED = "UNSOLICITED";
           - static const char *NONCE_BAD = "BAD";
           + static struct tmp_objdir *tmp_objdir;
           + 
           ++static struct proc_receive_ref {
           ++	unsigned int want_add:1,
           ++		     want_delete:1,
           ++		     want_modify:1;
           ++	char *ref_prefix;
           ++	struct proc_receive_ref *next;
           ++} *proc_receive_ref = NULL;
           ++
           ++static void proc_receive_ref_append(const char *prefix);
           ++
           + static enum deny_action parse_deny_action(const char *var, const char *value)
           + {
           + 	if (value) {
            @@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const char *value, void *cb)
             		return 0;
             	}
             
            +	if (strcmp(var, "receive.procreceiverefs") == 0) {
           -+		char *prefix;
           -+		int len;
           -+
            +		if (!value)
            +			return config_error_nonbool(var);
           -+		prefix = xstrdup(value);
           -+		len = strlen(prefix);
           -+		while (len && prefix[len - 1] == '/')
           -+			prefix[--len] = '\0';
           -+		string_list_append(&proc_receive_refs, prefix);
           ++		proc_receive_ref_append(value);
            +		return 0;
            +	}
            +
             	return git_default_config(var, value, cb);
             }
             
           +@@ builtin/receive-pack.c: struct command {
           + 	char ref_name[FLEX_ARRAY]; /* more */
           + };
           + 
           ++static void proc_receive_ref_append(const char *prefix)
           ++{
           ++	struct proc_receive_ref *ref_pattern;
           ++	char *p;
           ++	int len;
           ++
           ++	ref_pattern = xcalloc(1, sizeof(struct proc_receive_ref));
           ++	p = strchr(prefix, ':');
           ++	if (p) {
           ++		while (prefix < p) {
           ++			if (*prefix == 'a')
           ++				ref_pattern->want_add = 1;
           ++			else if (*prefix == 'd')
           ++				ref_pattern->want_delete = 1;
           ++			else if (*prefix == 'm')
           ++				ref_pattern->want_modify = 1;
           ++			prefix++;
           ++		}
           ++		prefix++;
           ++	} else {
           ++		ref_pattern->want_add = 1;
           ++		ref_pattern->want_delete = 1;
           ++		ref_pattern->want_modify = 1;
           ++	}
           ++	ref_pattern->next = NULL;
           ++	ref_pattern->ref_prefix = xstrdup(prefix);
           ++	len = strlen(ref_pattern->ref_prefix);
           ++	while (len && ref_pattern->ref_prefix[len - 1] == '/')
           ++		ref_pattern->ref_prefix[--len] = '\0';
           ++	if (proc_receive_ref == NULL) {
           ++		proc_receive_ref = ref_pattern;
           ++	} else {
           ++		struct proc_receive_ref *end;
           ++
           ++		end = proc_receive_ref;
           ++		while (end->next)
           ++			end = end->next;
           ++		end->next = ref_pattern;
           ++	}
           ++}
           ++
           ++static int proc_receive_ref_matches(struct command *cmd)
           ++{
           ++	struct proc_receive_ref *p;
           ++
           ++	if (!proc_receive_ref)
           ++		return 0;
           ++
           ++	for (p = proc_receive_ref; p; p = p->next) {
           ++		const char *match = p->ref_prefix;
           ++		int neg = 0;
           ++		const char *remains;
           ++
           ++		if (!p->want_add && is_null_oid(&cmd->old_oid))
           ++			continue;
           ++		else if (!p->want_delete && is_null_oid(&cmd->new_oid))
           ++			continue;
           ++		else if (!p->want_modify &&
           ++			 !is_null_oid(&cmd->old_oid) &&
           ++			 !is_null_oid(&cmd->new_oid))
           ++			continue;
           ++
           ++		if (*match == '!') {
           ++			neg = 1;
           ++			match++;
           ++		}
           ++
           ++		if (skip_prefix(cmd->ref_name, match, &remains) &&
           ++		    (!*remains || *remains == '/')) {
           ++			if (!neg)
           ++				return 1;
           ++		} else if (neg) {
           ++			return 1;
           ++		}
           ++	}
           ++	return 0;
           ++}
           ++
           + static void rp_error(const char *err, ...) __attribute__((format (printf, 1, 2)));
           + static void rp_warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
           + 
            @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
             	 * Try to find commands that have special prefix in their reference names,
             	 * and mark them to run an external "proc-receive" hook later.
           @@ builtin/receive-pack.c: static void execute_commands(struct command *commands,
            -	for (cmd = commands; cmd; cmd = cmd->next) {
            -		if (!should_process_cmd(cmd))
            -			continue;
           -+	if (proc_receive_refs.nr > 0) {
           -+		struct strbuf refname_full = STRBUF_INIT;
           -+		size_t prefix_len;
           -+
           -+		strbuf_addstr(&refname_full, get_git_namespace());
           -+		prefix_len = refname_full.len;
           ++	if (proc_receive_ref) {
           ++		for (cmd = commands; cmd; cmd = cmd->next) {
           ++			if (!should_process_cmd(cmd))
           ++				continue;
             
            -		/* TODO: replace the fixed prefix by looking up git config variables. */
            -		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
            -			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
            -			run_proc_receive = 1;
           -+		for (cmd = commands; cmd; cmd = cmd->next) {
           -+			if (!should_process_cmd(cmd))
           -+				continue;
           -+
           -+			strbuf_setlen(&refname_full, prefix_len);
           -+			strbuf_addstr(&refname_full, cmd->ref_name);
           -+			if (ref_matches(&proc_receive_refs, cmd->ref_name, refname_full.buf)) {
           ++			if (proc_receive_ref_matches(cmd)) {
            +				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
            +				run_proc_receive = 1;
            +			}
             		}
           -+
           -+		strbuf_release(&refname_full);
             	}
             
           - 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
           -@@ builtin/receive-pack.c: int cmd_receive_pack(int argc, const char **argv, const char *prefix)
           - 		OPT_END()
           - 	};
           - 
           -+	string_list_init(&proc_receive_refs, 0);
           -+
           - 	packet_trace_identity("receive-pack");
           - 
           - 	argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);
           -@@ builtin/receive-pack.c: int cmd_receive_pack(int argc, const char **argv, const char *prefix)
           - 	oid_array_clear(&shallow);
           - 	oid_array_clear(&ref);
           - 	free((void *)push_cert_nonce);
           -+	string_list_clear(&proc_receive_refs, 0);
           - 	return 0;
           - }
            
             ## t/t5411/once-0010-report-status-v1.sh ##
            @@
           @@ t/t5411/test-0041-process-all-refs--porcelain.sh (new)
            +		git update-ref -d refs/heads/baz
            +	)
            +'
           +
           + ## t/t5411/test-0050-proc-receive-refs.sh (new) ##
           +@@
           ++test_expect_success "config receive.procReceiveRefs with modifiers ($PROTOCOL)" '
           ++	(
           ++		cd "$upstream" &&
           ++		git config --unset-all receive.procReceiveRefs &&
           ++		git config --add receive.procReceiveRefs m:refs/heads/master &&
           ++		git config --add receive.procReceiveRefs ad:refs/heads &&
           ++		git config --add receive.procReceiveRefs "a:!refs/heads"
           ++	)
           ++'
           ++
           ++test_expect_success "setup proc-receive hook ($PROTOCOL)" '
           ++	write_script "$upstream/hooks/proc-receive" <<-EOF
           ++	printf >&2 "# proc-receive hook\n"
           ++	test-tool proc-receive -v \
           ++		-r "ok refs/heads/master" \
           ++		-r "option refname refs/pull/123/head" \
           ++		-r "option old-oid $A" \
           ++		-r "option new-oid $B" \
           ++		-r "ok refs/tags/v123 " \
           ++		-r "option refname refs/pull/124/head"
           ++	EOF
           ++'
           ++
           ++# Refs of upstream : master(A)
           ++# Refs of workbench: master(A)  tags/v123
           ++# git push         : master(B)  tags/v123
           ++test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
           ++	git -C workbench push origin \
           ++		$B:refs/heads/master \
           ++		v123 >out 2>&1 &&
           ++	make_user_friendly_and_stable_output <out >actual &&
           ++	cat >expect <<-EOF &&
           ++	remote: # pre-receive hook
           ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
           ++	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
           ++	remote: # proc-receive hook
           ++	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
           ++	remote: proc-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
           ++	remote: proc-receive> ok refs/heads/master
           ++	remote: proc-receive> option refname refs/pull/123/head
           ++	remote: proc-receive> option old-oid <COMMIT-A>
           ++	remote: proc-receive> option new-oid <COMMIT-B>
           ++	remote: proc-receive> ok refs/tags/v123
           ++	remote: proc-receive> option refname refs/pull/124/head
           ++	remote: # post-receive hook
           ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
           ++	remote: post-receive< <ZERO-OID> <TAG-v123> refs/pull/124/head
           ++	To <URL/of/upstream.git>
           ++	 <OID-A>..<OID-B> <COMMIT-B> -> refs/pull/123/head
           ++	 * [new reference] v123 -> refs/pull/124/head
           ++	EOF
           ++	test_cmp expect actual &&
           ++	git -C "$upstream" show-ref >out &&
           ++	make_user_friendly_and_stable_output <out >actual &&
           ++	cat >expect <<-EOF &&
           ++	<COMMIT-A> refs/heads/master
           ++	EOF
           ++	test_cmp expect actual
           ++'
           ++
           ++# Refs of upstream : master(A)
           ++# Refs of workbench: master(A)  tags/v123
           ++test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
           ++	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
           ++	git -C "$upstream" show-ref >out &&
           ++	make_user_friendly_and_stable_output <out >actual &&
           ++	cat >expect <<-EOF &&
           ++	<COMMIT-A> refs/heads/master
           ++	<TAG-v123> refs/tags/v123
           ++	EOF
           ++	test_cmp expect actual
           ++'
           ++
           ++test_expect_success "setup proc-receive hook ($PROTOCOL)" '
           ++	write_script "$upstream/hooks/proc-receive" <<-EOF
           ++	printf >&2 "# proc-receive hook\n"
           ++	test-tool proc-receive -v \
           ++		-r "ok refs/heads/master" \
           ++		-r "option refname refs/pull/123/head" \
           ++		-r "option old-oid $A" \
           ++		-r "option new-oid $ZERO_OID" \
           ++		-r "ok refs/heads/next" \
           ++		-r "option refname refs/pull/124/head" \
           ++		-r "option new-oid $A"
           ++	EOF
           ++'
           ++
           ++# Refs of upstream : master(A)  tags/v123
           ++# Refs of workbench: master(A)  tags/v123
           ++# git push         : NULL       NULL       next(A)
           ++test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOCOL)" '
           ++	git -C workbench push origin \
           ++		:refs/heads/master \
           ++		$A:refs/heads/next \
           ++		:refs/tags/v123 >out 2>&1 &&
           ++	make_user_friendly_and_stable_output <out >actual &&
           ++	cat >expect <<-EOF &&
           ++	remote: # pre-receive hook
           ++	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
           ++	remote: pre-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
           ++	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
           ++	remote: # proc-receive hook
           ++	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
           ++	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
           ++	remote: proc-receive> ok refs/heads/master
           ++	remote: proc-receive> option refname refs/pull/123/head
           ++	remote: proc-receive> option old-oid <COMMIT-A>
           ++	remote: proc-receive> option new-oid <ZERO-OID>
           ++	remote: proc-receive> ok refs/heads/next
           ++	remote: proc-receive> option refname refs/pull/124/head
           ++	remote: proc-receive> option new-oid <COMMIT-A>
           ++	remote: # post-receive hook
           ++	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/pull/123/head
           ++	remote: post-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
           ++	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/124/head
           ++	To <URL/of/upstream.git>
           ++	 - [deleted] refs/pull/123/head
           ++	 - [deleted] v123
           ++	 * [new reference] <COMMIT-A> -> refs/pull/124/head
           ++	EOF
           ++	test_cmp expect actual &&
           ++	git -C "$upstream" show-ref >out &&
           ++	make_user_friendly_and_stable_output <out >actual &&
           ++	cat >expect <<-EOF &&
           ++	<COMMIT-A> refs/heads/master
           ++	EOF
           ++	test_cmp expect actual
           ++'

---

Jiang Xin (10):
  transport: not report a non-head push as a branch
  t5411: add basic test cases for proc-receive hook
  receive-pack: add new proc-receive hook
  New capability "report-status-v2" for git-push
  doc: add document for capability report-status-v2
  receive-pack: feed report options to post-receive
  receive-pack: new config receive.procReceiveRefs
  t5411: test updates of remote-tracking branches
  transport: parse report options for tracking refs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt              |  19 +
 Documentation/githooks.txt                    |  62 ++
 Documentation/technical/pack-protocol.txt     |  43 +-
 .../technical/protocol-capabilities.txt       |  17 +-
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 528 ++++++++++++++++--
 builtin/send-pack.c                           |  21 +-
 remote.c                                      |   4 +-
 remote.h                                      |  16 +-
 send-pack.c                                   |  99 +++-
 t/helper/test-proc-receive.c                  | 176 ++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411-proc-receive-hook.sh                  | 117 ++++
 t/t5411/common-functions.sh                   |  56 ++
 t/t5411/once-0010-report-status-v1.sh         |  94 ++++
 t/t5411/test-0000-standard-git-push.sh        | 143 +++++
 .../test-0001-standard-git-push--porcelain.sh | 147 +++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0011-no-hook-error.sh            |  64 +++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 +++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++
 t/t5411/test-0020-report-ng.sh                |  67 +++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 +++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 ++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 ++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 +++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 +++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 +++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 +++++++++
 t/t5411/test-0034-report-ft.sh                |  44 ++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 ++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 227 ++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 172 ++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 +++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 +++
 t/t5411/test-0040-process-all-refs.sh         | 113 ++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 ++++
 t/t5411/test-0050-proc-receive-refs.sh        | 128 +++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport-helper.c                            |  53 +-
 transport.c                                   | 161 ++++--
 49 files changed, 4307 insertions(+), 107 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/once-0010-report-status-v1.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh
 create mode 100644 t/t5411/test-0050-proc-receive-refs.sh

-- 
2.28.0


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v17 01/10] transport: not report a non-head push as a branch
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
                                                           ` (8 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5516-fetch-push.sh | 2 +-
 transport.c           | 9 ++++++---
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 36ad20a849..d11382f769 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 2d4fd851dc..19b033dc6a 100644
--- a/transport.c
+++ b/transport.c
@@ -499,9 +499,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 02/10] t5411: add basic test cases for proc-receive hook
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 01/10] transport: not report a non-head push as a branch Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 03/10] receive-pack: add new " Jiang Xin
                                                           ` (7 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Topic "proc-receive-hook" will change the workflow and output of
git-push. Add some basic test cases in t5411 before introducing the new
topic.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh                  | 108 +++++++++++++
 t/t5411/common-functions.sh                   |  56 +++++++
 t/t5411/test-0000-standard-git-push.sh        | 143 +++++++++++++++++
 .../test-0001-standard-git-push--porcelain.sh | 147 ++++++++++++++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++++
 6 files changed, 521 insertions(+)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..3a684353a8
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+setup_upstream_and_workbench () {
+	# Refs of upstream : master(A)
+	# Refs of workbench: master(A)  tags/v123
+	test_expect_success "setup upstream and workbench" '
+		rm -rf upstream.git &&
+		rm -rf workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git tag -m "v123" v123 $A &&
+			git remote add origin ../upstream.git &&
+			git push origin master &&
+			git update-ref refs/heads/master $A $B &&
+			git -C ../upstream.git update-ref \
+				refs/heads/master $A $B
+		) &&
+		TAG=$(git -C workbench rev-parse v123) &&
+
+		# setup pre-receive hook
+		write_script upstream.git/hooks/pre-receive <<-\EOF &&
+		exec >&2
+		echo "# pre-receive hook"
+		while read old new ref
+		do
+			echo "pre-receive< $old $new $ref"
+		done
+		EOF
+
+		# setup post-receive hook
+		write_script upstream.git/hooks/post-receive <<-\EOF &&
+		exec >&2
+		echo "# post-receive hook"
+		while read old new ref
+		do
+			echo "post-receive< $old $new $ref"
+		done
+		EOF
+
+		upstream=upstream.git
+	'
+}
+
+run_proc_receive_hook_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	local)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Include test cases for both file and HTTP protocol
+	for t in  "$TEST_DIRECTORY"/t5411/test-*.sh
+	do
+		. "$t"
+	done
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run test cases for 'proc-receive' hook on local file protocol.
+run_proc_receive_hook_test local
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup for HTTP protocol" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin "$HTTPD_URL/auth-push/smart/upstream.git" &&
+	set_askpass user@host pass@host
+'
+
+setup_askpass_helper
+
+# Run test cases for 'proc-receive' hook on HTTP protocol.
+run_proc_receive_hook_test http
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6580bebd8e
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,56 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
new file mode 100644
index 0000000000..e206587348
--- /dev/null
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -0,0 +1,143 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> next
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [rejected] master -> master (non-fast-forward)
+	 ! [rejected] <COMMIT-B> -> next (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> next
+	 ! [rejected] master -> master (non-fast-forward)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> master -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] master -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
new file mode 100644
index 0000000000..48f6fcc846
--- /dev/null
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -0,0 +1,147 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/next    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --atomic --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "s/^# GETTEXT POISON #//" \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	! refs/heads/master:refs/heads/master [rejected] (non-fast-forward)
+	! <COMMIT-B>:refs/heads/next [rejected] (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/next    <OID-A>..<OID-B>
+	!    refs/heads/master:refs/heads/master    [rejected] (non-fast-forward)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	+    refs/heads/master:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	-    :refs/heads/next    [deleted]
+	*    refs/tags/v123:refs/tags/v123    [new tag]
+	*    refs/heads/master:refs/review/master/topic    [new reference]
+	*    HEAD:refs/heads/a/b/c    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
new file mode 100644
index 0000000000..c246f7e68e
--- /dev/null
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -0,0 +1,33 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (pre-receive hook declined)
+	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
new file mode 100644
index 0000000000..b14894de81
--- /dev/null
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (pre-receive hook declined)
+	!    HEAD:refs/heads/next    [remote rejected] (pre-receive hook declined)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 03/10] receive-pack: add new proc-receive hook
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (2 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-17 20:53                                           ` Junio C Hamano
  2020-08-15 17:17                                         ` [PATCH v17 04/10] New capability "report-status-v2" for git-push Jiang Xin
                                                           ` (6 subsequent siblings)
  10 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option fall-through)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status can be given in options
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option refname <refname>)
    H: PKT-LINE(option old-oid <old-oid>)
    H: PKT-LINE(option new-oid <new-oid>)
    H: PKT-LINE(option forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in option lines.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" will be
extended in latter commit just like what the "proc-receive" hook reports
to "receive-pack".

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 351 ++++++++++++++++--
 remote.h                                      |  14 +
 t/helper/test-proc-receive.c                  | 176 +++++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411/test-0011-no-hook-error.sh            |  64 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 ++++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++++
 t/t5411/test-0020-report-ng.sh                |  67 ++++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 ++++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 +++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 +++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 ++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 ++++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 +++++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 +++++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 +++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 +++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 159 ++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 162 ++++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 +++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 +++++
 28 files changed, 2659 insertions(+), 31 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh

diff --git a/Makefile b/Makefile
index 65f8cfb236..48c048584e 100644
--- a/Makefile
+++ b/Makefile
@@ -719,6 +719,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 439f29d6c7..d0b33ee84b 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -310,11 +310,14 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
-	const char *error_string;
+	struct ref_push_report report;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -775,7 +778,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	struct command *cmd = state->cmd;
 
 	while (cmd &&
-	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
+	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
@@ -840,6 +843,268 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(const struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return (struct command *)list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	int code = 0;
+	int new_options = 1;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *head;
+		const char *refname;
+		char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head);
+			code = -1;
+			continue;
+		}
+		*p++ = '\0';
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
+					new_options = 0;
+				}
+				code = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			else if (!strcmp(key, "fall-through"))
+				/* Fall through, let 'receive-pack' to execute it. */
+				hint->run_proc_receive = 0;
+			continue;
+		}
+
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    head, refname);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!strcmp(head, "ng")) {
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+			code = -1;
+		}
+		if (hint->run_proc_receive)
+			hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		new_options = 1;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->report.error_message &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->report.error_message = "proc-receive failed to report status";
+		    code = -1;
+		}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int hook_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					hook_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->report.error_message)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (hook_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1203,7 +1468,7 @@ static void run_update_post_hook(struct command *commands)
 		return;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string || cmd->did_not_exist)
+		if (cmd->report.error_message || cmd->did_not_exist)
 			continue;
 		if (!proc.args.nr)
 			strvec_push(&proc.args, hook);
@@ -1237,7 +1502,7 @@ static void check_aliased_update_internal(struct command *cmd,
 	if (!dst_name) {
 		rp_error("refusing update to broken symref '%s'", cmd->ref_name);
 		cmd->skip_update = 1;
-		cmd->error_string = "broken symref";
+		cmd->report.error_message = "broken symref";
 		return;
 	}
 	dst_name = strip_namespace(dst_name);
@@ -1264,7 +1529,7 @@ static void check_aliased_update_internal(struct command *cmd,
 		 find_unique_abbrev(&dst_cmd->old_oid, DEFAULT_ABBREV),
 		 find_unique_abbrev(&dst_cmd->new_oid, DEFAULT_ABBREV));
 
-	cmd->error_string = dst_cmd->error_string =
+	cmd->report.error_message = dst_cmd->report.error_message =
 		"inconsistent aliased update";
 }
 
@@ -1293,7 +1558,7 @@ static void check_aliased_updates(struct command *commands)
 	string_list_sort(&ref_list);
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			check_aliased_update(cmd, &ref_list);
 	}
 
@@ -1330,7 +1595,7 @@ static void set_connectivity_errors(struct command *commands,
 				     &opt))
 			continue;
 
-		cmd->error_string = "missing necessary objects";
+		cmd->report.error_message = "missing necessary objects";
 	}
 }
 
@@ -1369,7 +1634,7 @@ static void reject_updates_to_hidden(struct command *commands)
 	prefix_len = refname_full.len;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			continue;
 
 		strbuf_setlen(&refname_full, prefix_len);
@@ -1378,9 +1643,9 @@ static void reject_updates_to_hidden(struct command *commands)
 		if (!ref_is_hidden(cmd->ref_name, refname_full.buf))
 			continue;
 		if (is_null_oid(&cmd->new_oid))
-			cmd->error_string = "deny deleting a hidden ref";
+			cmd->report.error_message = "deny deleting a hidden ref";
 		else
-			cmd->error_string = "deny updating a hidden ref";
+			cmd->report.error_message = "deny updating a hidden ref";
 	}
 
 	strbuf_release(&refname_full);
@@ -1388,7 +1653,7 @@ static void reject_updates_to_hidden(struct command *commands)
 
 static int should_process_cmd(struct command *cmd)
 {
-	return !cmd->error_string && !cmd->skip_update;
+	return !cmd->report.error_message && !cmd->skip_update;
 }
 
 static void warn_if_skipped_connectivity_check(struct command *commands,
@@ -1415,24 +1680,24 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
 		if (!transaction) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "transaction failed to start";
+			cmd->report.error_message = "transaction failed to start";
 			continue;
 		}
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (!cmd->error_string
+		if (!cmd->report.error_message
 		    && ref_transaction_commit(transaction, &err)) {
 			rp_error("%s", err.buf);
 			strbuf_reset(&err);
-			cmd->error_string = "failed to update ref";
+			cmd->report.error_message = "failed to update ref";
 		}
 		ref_transaction_free(transaction);
 	}
@@ -1455,12 +1720,12 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
-		cmd->error_string = update(cmd, si);
+		cmd->report.error_message = update(cmd, si);
 
-		if (cmd->error_string)
+		if (cmd->report.error_message)
 			goto failure;
 	}
 
@@ -1473,8 +1738,8 @@ static void execute_commands_atomic(struct command *commands,
 
 failure:
 	for (cmd = commands; cmd; cmd = cmd->next)
-		if (!cmd->error_string)
-			cmd->error_string = reported_error;
+		if (!cmd->report.error_message)
+			cmd->report.error_message = reported_error;
 
 cleanup:
 	ref_transaction_free(transaction);
@@ -1491,10 +1756,11 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
-			cmd->error_string = "unpacker error";
+			cmd->report.error_message = "unpacker error";
 		return;
 	}
 
@@ -1520,10 +1786,25 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "pre-receive hook declined";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "pre-receive hook declined";
 		}
 		return;
 	}
@@ -1534,8 +1815,8 @@ static void execute_commands(struct command *commands,
 	 */
 	if (tmp_objdir_migrate(tmp_objdir) < 0) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
-			if (!cmd->error_string)
-				cmd->error_string = "unable to migrate objects to permanent storage";
+			if (!cmd->report.error_message)
+				cmd->report.error_message = "unable to migrate objects to permanent storage";
 		}
 		return;
 	}
@@ -1546,6 +1827,14 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->report.error_message &&
+			    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED) &&
+			    (cmd->run_proc_receive || use_atomic))
+				cmd->report.error_message = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
@@ -1916,7 +2205,7 @@ static void update_shallow_info(struct command *commands,
 		if (is_null_oid(&cmd->new_oid))
 			continue;
 		if (ref_status[cmd->index]) {
-			cmd->error_string = "shallow update not allowed";
+			cmd->report.error_message = "shallow update not allowed";
 			cmd->skip_update = 1;
 		}
 	}
@@ -1931,12 +2220,12 @@ static void report(struct command *commands, const char *unpack_status)
 	packet_buf_write(&buf, "unpack %s\n",
 			 unpack_status ? unpack_status : "ok");
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!cmd->error_string)
+		if (!cmd->report.error_message)
 			packet_buf_write(&buf, "ok %s\n",
 					 cmd->ref_name);
 		else
 			packet_buf_write(&buf, "ng %s %s\n",
-					 cmd->ref_name, cmd->error_string);
+					 cmd->ref_name, cmd->report.error_message);
 	}
 	packet_buf_flush(&buf);
 
@@ -2040,7 +2329,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 		if (!check_cert_push_options(&push_options)) {
 			struct command *cmd;
 			for (cmd = commands; cmd; cmd = cmd->next)
-				cmd->error_string = "inconsistent push options";
+				cmd->report.error_message = "inconsistent push options";
 		}
 
 		prepare_shallow_info(&si, &shallow);
diff --git a/remote.h b/remote.h
index 5e3ea5a26d..ab2e86a2a5 100644
--- a/remote.h
+++ b/remote.h
@@ -93,6 +93,20 @@ int for_each_remote(each_remote_fn fn, void *priv);
 
 int remote_has_url(struct remote *remote, const char *url);
 
+struct ref_push_report_options {
+	const char *ref_name;
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	unsigned int forced_update:1;
+	struct ref_push_report_options *next;
+};
+
+struct ref_push_report {
+	const char *message;
+	const char *error_message;
+	struct ref_push_report_options *options;
+};
+
 struct ref {
 	struct ref *next;
 	struct object_id old_oid;
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..4f352a7baf
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,176 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int die_version = 0;
+static int die_readline = 0;
+static int no_push_options = 0;
+static int use_atomic = 0;
+static int use_push_options = 0;
+static int verbose = 0;
+static int version = 1;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1 || die_version)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ' ||
+		    die_readline)
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	int nongit_ok = 0;
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_BOOL(0, "die-version", &die_version,
+			 "die during version negotiation"),
+		OPT_BOOL(0, "die-readline", &die_readline,
+			 "die when readline"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	setup_git_directory_gently(&nongit_ok);
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 590b2efca7..a0d3966b29 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -46,6 +46,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ddc8e990e9..07034d3f38 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -35,6 +35,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
new file mode 100644
index 0000000000..bb6ec92a92
--- /dev/null
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -0,0 +1,64 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
new file mode 100644
index 0000000000..4814f74dc2
--- /dev/null
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -0,0 +1,66 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
new file mode 100644
index 0000000000..c5fe4cb37b
--- /dev/null
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -0,0 +1,217 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: bad protocol version: 1
+	remote: error: proc-receive version "0" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
new file mode 100644
index 0000000000..53b47b0185
--- /dev/null
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -0,0 +1,160 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
new file mode 100644
index 0000000000..f726b7ca9c
--- /dev/null
+++ b/t/t5411/test-0020-report-ng.sh
@@ -0,0 +1,67 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
new file mode 100644
index 0000000000..fbf5569103
--- /dev/null
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -0,0 +1,69 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
new file mode 100644
index 0000000000..92a415b929
--- /dev/null
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
new file mode 100644
index 0000000000..acbf93e40a
--- /dev/null
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -0,0 +1,46 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
new file mode 100644
index 0000000000..c3946f329a
--- /dev/null
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
new file mode 100644
index 0000000000..d093b1a579
--- /dev/null
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
new file mode 100644
index 0000000000..d0c4da8b23
--- /dev/null
+++ b/t/t5411/test-0026-push-options.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
new file mode 100644
index 0000000000..c89a1e7c57
--- /dev/null
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -0,0 +1,82 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push \
+		--porcelain \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
new file mode 100644
index 0000000000..44c99d3831
--- /dev/null
+++ b/t/t5411/test-0030-report-ok.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
new file mode 100644
index 0000000000..3223b26184
--- /dev/null
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -0,0 +1,36 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
new file mode 100644
index 0000000000..a743aa8018
--- /dev/null
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -0,0 +1,256 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
new file mode 100644
index 0000000000..439b97b06e
--- /dev/null
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -0,0 +1,265 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/for/a/b/c/topic    [new reference]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
new file mode 100644
index 0000000000..aca2b0676c
--- /dev/null
+++ b/t/t5411/test-0034-report-ft.sh
@@ -0,0 +1,44 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
new file mode 100644
index 0000000000..30ffffb352
--- /dev/null
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (fall-through, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    <COMMIT-B>:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
new file mode 100644
index 0000000000..12acf9ea1f
--- /dev/null
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -0,0 +1,159 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
new file mode 100644
index 0000000000..f4a2c56e14
--- /dev/null
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -0,0 +1,162 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
new file mode 100644
index 0000000000..1dd6a97a97
--- /dev/null
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -0,0 +1,89 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic2
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
+	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
new file mode 100644
index 0000000000..32ebf63dcf
--- /dev/null
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -0,0 +1,91 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/bar    [new branch]
+	*    HEAD:refs/heads/baz    [new branch]
+	*    HEAD:refs/for/next/topic2    [new reference]
+	*    HEAD:refs/heads/foo    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
+	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 04/10] New capability "report-status-v2" for git-push
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (3 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 03/10] receive-pack: add new " Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-17 21:12                                           ` Junio C Hamano
  2020-08-15 17:17                                         ` [PATCH v17 05/10] doc: add document for capability report-status-v2 Jiang Xin
                                                           ` (5 subsequent siblings)
  10 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin, Jeff King

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The new introduced "proc-receive" hook may handle a command for a
pseudo-reference with a zero-old as its old-oid, while the hook may
create or update a reference with different name, different new-oid,
and different old-oid (the reference may exist already with a non-zero
old-oid).  Current "report-status" protocol cannot report the status for
such reference rewrite.

Add new capability "report-status-v2" and new report protocol which is
not backward compatible for report of git-push.

If a user pushes to a pseudo-reference "refs/for/master/topic", and
"receive-pack" creates two new references "refs/changes/23/123/1" and
"refs/changes/24/124/1", for client without the knowledge of
"report-status-v2", "receive-pack" will only send "ok/ng" directives in
the report, such as:

    ok ref/for/master/topic

But for client which has the knowledge of "report-status-v2",
"receive-pack" will use "option" directives to report more attributes
for the reference given by the above "ok/ng" directive.

    ok refs/for/master/topic
    option refname refs/changes/23/123/1
    option new-oid <new-oid>
    ok refs/for/master/topic
    option refname refs/changes/24/124/1
    option new-oid <new-oid>

The client will report two new created references to the end user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        |  52 +++++++-
 builtin/send-pack.c                           |  21 +++-
 remote.c                                      |   4 +-
 remote.h                                      |   2 +-
 send-pack.c                                   |  99 ++++++++++++---
 t/t5411-proc-receive-hook.sh                  |   9 ++
 t/t5411/once-0010-report-status-v1.sh         |  89 +++++++++++++
 t/t5411/test-0032-report-with-options.sh      |  14 +--
 ...est-0033-report-with-options--porcelain.sh |  14 +--
 ...t-0036-report-multi-rewrite-for-one-ref.sh |  11 +-
 ...rt-multi-rewrite-for-one-ref--porcelain.sh |  11 +-
 t/t5411/test-0038-report-mixed-refs.sh        |   2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |   2 +-
 transport-helper.c                            |  53 +++++++-
 transport.c                                   | 118 +++++++++++++-----
 15 files changed, 424 insertions(+), 77 deletions(-)
 create mode 100644 t/t5411/once-0010-report-status-v1.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index d0b33ee84b..58972aa7cf 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -57,6 +57,7 @@ static int advertise_push_options;
 static int unpack_limit = 100;
 static off_t max_input_size;
 static int report_status;
+static int report_status_v2;
 static int use_sideband;
 static int use_atomic;
 static int use_push_options;
@@ -240,7 +241,7 @@ static void show_ref(const char *path, const struct object_id *oid)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status report-status-v2 delete-refs side-band-64k quiet");
 		if (advertise_atomic_push)
 			strbuf_addstr(&cap, " atomic");
 		if (prefer_ofs_delta)
@@ -1918,6 +1919,8 @@ static struct command *read_head_info(struct packet_reader *reader,
 			int len = 0;
 			if (parse_feature_request(feature_list, "report-status"))
 				report_status = 1;
+			if (parse_feature_request(feature_list, "report-status-v2"))
+				report_status_v2 = 1;
 			if (parse_feature_request(feature_list, "side-band-64k"))
 				use_sideband = LARGE_PACKET_MAX;
 			if (parse_feature_request(feature_list, "quiet"))
@@ -2236,6 +2239,49 @@ static void report(struct command *commands, const char *unpack_status)
 	strbuf_release(&buf);
 }
 
+static void report_v2(struct command *commands, const char *unpack_status)
+{
+	struct command *cmd;
+	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
+
+	packet_buf_write(&buf, "unpack %s\n",
+			 unpack_status ? unpack_status : "ok");
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		int count = 0;
+
+		if (!cmd->report.error_message)
+			packet_buf_write(&buf, "ok %s\n",
+					 cmd->ref_name);
+		else
+			packet_buf_write(&buf, "ng %s %s\n",
+					 cmd->ref_name,
+					 cmd->report.error_message);
+		for (options = cmd->report.options; options; options = options->next) {
+			if (count++ > 0)
+				packet_buf_write(&buf, "ok %s\n", cmd->ref_name);
+			if (options->ref_name)
+				packet_buf_write(&buf, "option refname %s\n",
+						 options->ref_name);
+			if (options->old_oid)
+				packet_buf_write(&buf, "option old-oid %s\n",
+						 oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				packet_buf_write(&buf, "option new-oid %s\n",
+						 oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				packet_buf_write(&buf, "option forced-update\n");
+		}
+	}
+	packet_buf_flush(&buf);
+
+	if (use_sideband)
+		send_sideband(1, 1, buf.buf, buf.len, use_sideband);
+	else
+		write_or_die(1, buf.buf, buf.len);
+	strbuf_release(&buf);
+}
+
 static int delete_only(struct command *commands)
 {
 	struct command *cmd;
@@ -2344,7 +2390,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
-		if (report_status)
+		if (report_status_v2)
+			report_v2(commands, unpack_status);
+		else if (report_status)
 			report(commands, unpack_status);
 		run_receive_hook(commands, "post-receive", 1,
 				 &push_options);
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 2b9610f121..9d0df751d6 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -29,10 +29,12 @@ static struct send_pack_args args;
 static void print_helper_status(struct ref *ref)
 {
 	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report_options *options;
 
 	for (; ref; ref = ref->next) {
 		const char *msg = NULL;
 		const char *res;
+		int count = 0;
 
 		switch(ref->status) {
 		case REF_STATUS_NONE:
@@ -86,14 +88,29 @@ static void print_helper_status(struct ref *ref)
 
 		strbuf_reset(&buf);
 		strbuf_addf(&buf, "%s %s", res, ref->name);
-		if (ref->remote_status)
-			msg = ref->remote_status;
+		if (ref->report.error_message)
+			msg = ref->report.error_message;
 		if (msg) {
 			strbuf_addch(&buf, ' ');
 			quote_two_c_style(&buf, "", msg, 0);
 		}
 		strbuf_addch(&buf, '\n');
 
+		for (options = ref->report.options; options; options = options->next) {
+			if (count++ > 0)
+				strbuf_addf(&buf, "ok %s\n", ref->name);
+			if (options->ref_name)
+				strbuf_addf(&buf, "option refname %s\n",
+					    options->ref_name);
+			if (options->old_oid)
+				strbuf_addf(&buf, "option old-oid %s\n",
+					    oid_to_hex(options->old_oid));
+			if (options->new_oid)
+				strbuf_addf(&buf, "option new-oid %s\n",
+					    oid_to_hex(options->new_oid));
+			if (options->forced_update)
+				strbuf_addstr(&buf, "option forced-update\n");
+		}
 		write_or_die(1, buf.buf, buf.len);
 	}
 	strbuf_release(&buf);
diff --git a/remote.c b/remote.c
index c5ed74f91c..682be35fb2 100644
--- a/remote.c
+++ b/remote.c
@@ -789,7 +789,7 @@ struct ref *copy_ref(const struct ref *ref)
 	memcpy(cpy, ref, len);
 	cpy->next = NULL;
 	cpy->symref = xstrdup_or_null(ref->symref);
-	cpy->remote_status = xstrdup_or_null(ref->remote_status);
+	cpy->report.error_message = xstrdup_or_null(ref->report.error_message);
 	cpy->peer_ref = copy_ref(ref->peer_ref);
 	return cpy;
 }
@@ -811,7 +811,7 @@ void free_one_ref(struct ref *ref)
 	if (!ref)
 		return;
 	free_one_ref(ref->peer_ref);
-	free(ref->remote_status);
+	free((void *)ref->report.error_message);
 	free(ref->symref);
 	free(ref);
 }
diff --git a/remote.h b/remote.h
index ab2e86a2a5..1a7b53fc16 100644
--- a/remote.h
+++ b/remote.h
@@ -153,7 +153,7 @@ struct ref {
 		REF_STATUS_EXPECTING_REPORT,
 		REF_STATUS_ATOMIC_PUSH_FAILED
 	} status;
-	char *remote_status;
+	struct ref_push_report report;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
 };
diff --git a/send-pack.c b/send-pack.c
index 632f1580ca..5e25352042 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -154,25 +154,79 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 {
 	struct ref *hint;
 	int ret;
+	int new_options = 1;
 
 	hint = NULL;
 	ret = receive_unpack_status(reader);
 	while (1) {
+		struct object_id old_oid, new_oid;
+		const char *head;
 		const char *refname;
-		char *msg;
+		char *p;
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
-		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
-			error("invalid ref status from remote: %s", reader->line);
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			error("invalid status line from remote: %s", reader->line);
 			ret = -1;
 			break;
 		}
+		*p++ = '\0';
+
+		if (!strcmp(head, "option")) {
+			struct ref_push_report_options *options;
+			const char *key, *val;
+
+			if (!hint) {
+			       if (new_options) {
+					error("'option' without a matching 'ok/ng' directive");
+					new_options = 0;
+				}
+				ret = -1;
+				continue;
+			}
+			options = hint->report.options;
+			while (options && options->next)
+				options = options->next;
+			if (new_options) {
+				if (!options) {
+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = hint->report.options;
+				} else {
+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+					options = options->next;
+				}
+				new_options = 0;
+			}
+			assert(options);
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				options->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				options->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				options->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				options->forced_update = 1;
+			continue;
+		}
 
-		refname = reader->line + 3;
-		msg = strchr(refname, ' ');
-		if (msg)
-			*msg++ = '\0';
-
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			error("invalid ref status from remote: %s", head);
+			ret = -1;
+			break;
+		}
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
 			hint = find_ref_by_name(hint, refname);
@@ -183,19 +237,24 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 					refname);
 			continue;
 		}
-		if (hint->status != REF_STATUS_EXPECTING_REPORT) {
+		if (hint->status != REF_STATUS_EXPECTING_REPORT &&
+		    hint->status != REF_STATUS_OK &&
+		    hint->status != REF_STATUS_REMOTE_REJECT) {
 			warning("remote reported status on unexpected ref: %s",
 					refname);
 			continue;
 		}
-
-		if (reader->line[0] == 'o' && reader->line[1] == 'k')
-			hint->status = REF_STATUS_OK;
-		else
+		if (!strcmp(head, "ng")) {
 			hint->status = REF_STATUS_REMOTE_REJECT;
-		hint->remote_status = xstrdup_or_null(msg);
-		/* start our next search from the next ref */
-		hint = hint->next;
+			if (p)
+				hint->report.error_message = xstrdup(p);
+			else
+				hint->report.error_message = "failed";
+		} else {
+			hint->status = REF_STATUS_OK;
+			hint->report.message = xstrdup_or_null(p);
+		}
+		new_options = 1;
 	}
 	return ret;
 }
@@ -371,7 +430,9 @@ int send_pack(struct send_pack_args *args,
 	struct packet_reader reader;
 
 	/* Does the other end support the reporting? */
-	if (server_supports("report-status"))
+	if (server_supports("report-status-v2"))
+		status_report = 2;
+	else if (server_supports("report-status"))
 		status_report = 1;
 	if (server_supports("delete-refs"))
 		allow_deleting_refs = 1;
@@ -423,8 +484,10 @@ int send_pack(struct send_pack_args *args,
 
 	use_push_options = push_options_supported && args->push_options;
 
-	if (status_report)
+	if (status_report == 1)
 		strbuf_addstr(&cap_buf, " report-status");
+	else if (status_report == 2)
+		strbuf_addstr(&cap_buf, " report-status-v2");
 	if (use_sideband)
 		strbuf_addstr(&cap_buf, " side-band-64k");
 	if (quiet_supported && (args->quiet || !args->progress))
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 3a684353a8..746487286f 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -78,6 +78,15 @@ run_proc_receive_hook_test() {
 # Initialize the upstream repository and local workbench.
 setup_upstream_and_workbench
 
+# Load test cases that only need to be executed once.
+for t in  "$TEST_DIRECTORY"/t5411/once-*.sh
+do
+	. "$t"
+done
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
 # Run test cases for 'proc-receive' hook on local file protocol.
 run_proc_receive_hook_test local
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
new file mode 100644
index 0000000000..56a86d8ed4
--- /dev/null
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -0,0 +1,89 @@
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic1" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/23/123/1" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/24/124/2" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update" \
+		-r "ng refs/for/next/topic target branch not exist"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic1(A)  foo(A)  refs/for/next/topic(A)  refs/for/master/topic2(A)
+test_expect_success "proc-receive: report status v1" '
+	{
+		if test -z "$GIT_DEFAULT_HASH" || test "$GIT_DEFAULT_HASH" = "sha1"
+		then
+			printf "%s %s refs/heads/master\0report-status\n" \
+				$A $B | packetize
+		else
+			printf "%s %s refs/heads/master\0report-status object-format=$GIT_DEFAULT_HASH\n" \
+				$A $B | packetize
+		fi &&
+		printf "%s %s refs/for/master/topic1\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/heads/foo\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/next/topic\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/master/topic2\n" \
+			$ZERO_OID $A | packetize &&
+		printf 0000 &&
+		printf "" | git -C "$upstream" pack-objects --stdout
+	} | git receive-pack "$upstream" --stateless-rpc \
+	>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	# pre-receive hook
+	pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	# proc-receive hook
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	proc-receive> ok refs/for/master/topic1
+	proc-receive> option fall-through
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/23/123/1
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/24/124/2
+	proc-receive> option old-oid <COMMIT-B>
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> option forced-update
+	proc-receive> ng refs/for/next/topic target branch not exist
+	000eunpack ok
+	0019ok refs/heads/master
+	001eok refs/for/master/topic1
+	0016ok refs/heads/foo
+	0033ng refs/for/next/topic target branch not exist
+	001eok refs/for/master/topic2
+	0000# post-receive hook
+	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/for/master/topic1
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a743aa8018..3742ed3c3d 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -56,7 +56,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -89,7 +89,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -123,7 +123,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -155,7 +155,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -189,7 +189,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -241,9 +241,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 439b97b06e..f18ba9f06f 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -57,7 +57,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -92,7 +92,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -127,7 +127,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -160,7 +160,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -195,7 +195,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual
@@ -249,9 +249,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
-	*    HEAD:refs/for/master/topic    [new reference]
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 12acf9ea1f..64662afdc6 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -147,7 +151,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index f4a2c56e14..a7e29af0c2 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -43,7 +43,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/23/123/1    [new reference]
+	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 1dd6a97a97..54bec4c026 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic2
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 32ebf63dcf..605b3cb5c9 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	*    HEAD:refs/heads/baz    [new branch]
 	*    HEAD:refs/for/next/topic2    [new reference]
 	*    HEAD:refs/heads/foo    [new branch]
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
 	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
 	Done
diff --git a/transport-helper.c b/transport-helper.c
index defafbf4c1..0029ba18bd 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -729,6 +729,49 @@ static int push_update_ref_status(struct strbuf *buf,
 {
 	char *refname, *msg;
 	int status, forced = 0;
+	static struct ref *hint = NULL;
+	static int new_options = 1;
+
+	if (starts_with(buf->buf, "option ")) {
+		struct ref_push_report_options *options;
+		struct object_id old_oid, new_oid;
+		const char *key, *val;
+		char *p;
+
+		if (!hint)
+			die(_("'option' without a matching 'ok/error' directive"));
+		options = hint->report.options;
+		while (options && options->next)
+			options = options->next;
+		if (new_options) {
+			if (!options) {
+				hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = hint->report.options;
+			} else {
+				options->next = xcalloc(1, sizeof(struct ref_push_report_options));
+				options = options->next;
+			}
+			new_options = 0;
+		}
+		assert(options);
+		key = buf->buf + 7;
+		p = strchr(key, ' ');
+		if (p)
+			*p++ = '\0';
+		val = p;
+		if (!strcmp(key, "refname"))
+			options->ref_name = xstrdup_or_null(val);
+		else if (!strcmp(key, "old-oid") && val &&
+			 !parse_oid_hex(val, &old_oid, &val))
+			options->old_oid = oiddup(&old_oid);
+		else if (!strcmp(key, "new-oid") && val &&
+			 !parse_oid_hex(val, &new_oid, &val))
+			options->new_oid = oiddup(&new_oid);
+		else if (!strcmp(key, "forced-update"))
+			options->forced_update = 1;
+		/* Not update remote namespace again. */
+		return 1;
+	}
 
 	if (starts_with(buf->buf, "ok ")) {
 		status = REF_STATUS_OK;
@@ -791,8 +834,11 @@ static int push_update_ref_status(struct strbuf *buf,
 		*ref = find_ref_by_name(remote_refs, refname);
 	if (!*ref) {
 		warning(_("helper reported unexpected status of %s"), refname);
+		hint = NULL;
 		return 1;
 	}
+	hint = *ref;
+	new_options = 1;
 
 	if ((*ref)->status != REF_STATUS_NONE) {
 		/*
@@ -805,7 +851,12 @@ static int push_update_ref_status(struct strbuf *buf,
 
 	(*ref)->status = status;
 	(*ref)->forced_update |= forced;
-	(*ref)->remote_status = msg;
+	if (msg) {
+		if (status == REF_STATUS_OK)
+			(*ref)->report.message = msg;
+		else
+			(*ref)->report.error_message = msg;
+	}
 	return !(status == REF_STATUS_OK);
 }
 
diff --git a/transport.c b/transport.c
index 19b033dc6a..10f7dbbcd0 100644
--- a/transport.c
+++ b/transport.c
@@ -460,13 +460,21 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
+			     struct ref_push_report_options *options,
 			     int porcelain, int summary_width)
 {
+	const char *to_name;
+
+	if (options && options->ref_name)
+		to_name = options->ref_name;
+	else
+		to_name = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -480,9 +488,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -492,27 +502,52 @@ static void print_ref_status(char flag, const char *summary,
 	}
 }
 
-static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
+static void print_ok_ref_status(struct ref *ref,
+				struct ref_push_report_options *options,
+				int porcelain, int summary_width)
 {
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	const char *ref_name;
+	int forced_update;
+
+	if (options && options->old_oid)
+		old_oid = options->old_oid;
+	else
+		old_oid = &ref->old_oid;
+	if (options && options->new_oid)
+		new_oid = options->new_oid;
+	else
+		new_oid = &ref->new_oid;
+	if (options && options->forced_update)
+		forced_update = options->forced_update;
+	else
+		forced_update = ref->forced_update;
+	if (options && options->ref_name)
+		ref_name = options->ref_name;
+	else
+		ref_name = ref->name;
+
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
-				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+				 options, porcelain, summary_width);
+	else if (is_null_oid(old_oid))
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(ref_name, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(ref_name, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 ref, ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
 
-		strbuf_add_unique_abbrev(&quickref, &ref->old_oid,
+		strbuf_add_unique_abbrev(&quickref, old_oid,
 					 DEFAULT_ABBREV);
-		if (ref->forced_update) {
+		if (forced_update) {
 			strbuf_addstr(&quickref, "...");
 			type = '+';
 			msg = "forced update";
@@ -521,17 +556,18 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 			type = ' ';
 			msg = NULL;
 		}
-		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
+		strbuf_add_unique_abbrev(&quickref, new_oid,
 					 DEFAULT_ABBREV);
 
 		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
-				 int porcelain, int summary_width)
+static int _print_one_push_status(struct ref *ref, const char *dest, int count,
+				  struct ref_push_report_options *options,
+				  int porcelain, int summary_width)
 {
 	if (!count) {
 		char *url = transport_anonymize_url(dest);
@@ -542,65 +578,89 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	switch(ref->status) {
 	case REF_STATUS_NONE:
 		print_ref_status('X', "[no match]", ref, NULL, NULL,
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
 		print_ref_status('!', "[rejected]", ref, NULL,
 				 "remote does not support deleting refs",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
 		print_ref_status('=', "[up to date]", ref,
-				 ref->peer_ref, NULL, porcelain, summary_width);
+				 ref->peer_ref, NULL,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "non-fast-forward", porcelain, summary_width);
+				 "non-fast-forward",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "already exists", porcelain, summary_width);
+				 "already exists",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "fetch first", porcelain, summary_width);
+				 "fetch first",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "needs force", porcelain, summary_width);
+				 "needs force",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "stale info", porcelain, summary_width);
+				 "stale info",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
 				 "new shallow roots not allowed",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
 		print_ref_status('!', "[remote rejected]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
-				 ref->remote_status, porcelain, summary_width);
+				 ref->report.error_message,
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
 		print_ref_status('!', "[remote failure]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
-				 porcelain, summary_width);
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "atomic push failed", porcelain, summary_width);
+				 "atomic push failed",
+				 options, porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-		print_ok_ref_status(ref, porcelain, summary_width);
+		print_ok_ref_status(ref, options, porcelain, summary_width);
 		break;
 	}
 
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	struct ref_push_report_options *options;
+	int n = 0;
+
+	if (!ref->report.options)
+		return _print_one_push_status(ref, dest, count,
+					      NULL, porcelain, summary_width);
+
+	for (options = ref->report.options; options; options = options->next)
+		_print_one_push_status(ref, dest, count + n++,
+				       options, porcelain, summary_width);
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 05/10] doc: add document for capability report-status-v2
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (4 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 04/10] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 06/10] receive-pack: feed report options to post-receive Jiang Xin
                                                           ` (4 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add ABNF notation for capability 'report-status-v2' which extends
capability 'report-status' by adding additional option lines.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/technical/pack-protocol.txt     | 43 +++++++++++++++++--
 .../technical/protocol-capabilities.txt       | 17 ++++++--
 2 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt
index a4573d12ce..01eb1629a8 100644
--- a/Documentation/technical/pack-protocol.txt
+++ b/Documentation/technical/pack-protocol.txt
@@ -503,8 +503,8 @@ The reference discovery phase is done nearly the same way as it is in the
 fetching protocol. Each reference obj-id and name on the server is sent
 in packet-line format to the client, followed by a flush-pkt.  The only
 real difference is that the capability listing is different - the only
-possible values are 'report-status', 'delete-refs', 'ofs-delta' and
-'push-options'.
+possible values are 'report-status', 'report-status-v2', 'delete-refs',
+'ofs-delta', 'atomic' and 'push-options'.
 
 Reference Update Request and Packfile Transfer
 ----------------------------------------------
@@ -625,7 +625,7 @@ Report Status
 -------------
 
 After receiving the pack data from the sender, the receiver sends a
-report if 'report-status' capability is in effect.
+report if 'report-status' or 'report-status-v2' capability is in effect.
 It is a short listing of what happened in that update.  It will first
 list the status of the packfile unpacking as either 'unpack ok' or
 'unpack [error]'.  Then it will list the status for each of the references
@@ -647,6 +647,43 @@ update was successful, or 'ng [refname] [error]' if the update was not.
   error-msg         = 1*(OCTET) ; where not "ok"
 ----
 
+New capability "report-status-v2" extends the protocol by adding new option
+lines in order to support reporting of reference rewritten by the
+"proc-receive" hook.  The "proc-receive" hook may handle a command for a
+pseudo-reference which may create or update one or more references, and each
+reference may have different name, different new-oid, and different old-oid.
+
+----
+  report-status-v2  = unpack-status
+		      1*(command-status-v2)
+		      flush-pkt
+
+  unpack-status     = PKT-LINE("unpack" SP unpack-result)
+  unpack-result     = "ok" / error-msg
+
+  command-status-v2 = command-ok-v2 / command-fail-v2
+  command-ok-v2     = command-ok
+		      *option-line
+  command-fail-v2   = command-fail
+		      *option-line
+
+  command-ok        = PKT-LINE("ok" SP refname)
+  command-fail      = PKT-LINE("ng" SP refname SP error-msg)
+
+  error-msg         = 1*(OCTET) ; where not "ok"
+
+  option-line       = *1(option-refname)
+		      *1(option-old-oid)
+		      *1(option-new-oid)
+		      *1(option-forced-update)
+
+  option-refname    = PKT-LINE("option" SP "refname" SP refname)
+  option-old-oid    = PKT-LINE("option" SP "old-oid" SP obj-id)
+  option-new-oid    = PKT-LINE("option" SP "new-oid" SP obj-id)
+  option-force      = PKT-LINE("option" SP "forced-update")
+
+----
+
 Updates can be unsuccessful for a number of reasons.  The reference can have
 changed since the reference discovery phase was originally sent, meaning
 someone pushed in the meantime.  The reference being pushed could be a
diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 36ccd14f97..78c0c4b3e2 100644
--- a/Documentation/technical/protocol-capabilities.txt
+++ b/Documentation/technical/protocol-capabilities.txt
@@ -22,9 +22,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 'atomic', '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', 'report-status-v2', '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
@@ -284,6 +284,17 @@ each reference was updated successfully.  If any of those were not
 successful, it will send back an error message.  See pack-protocol.txt
 for example messages.
 
+report-status-v2
+----------------
+
+Capability 'report-status-v2' extends capability 'report-status' by
+adding new option lines in order to support reference rewritten by
+the"proc-receive" hook.  The "proc-receive" hook may handle a command
+for a pseudo-reference which may create or update a reference with
+different name, new-oid, and old-oid.  While the capability
+"report-status" cannot report for such case.  See pack-protocol.txt
+for details.
+
 delete-refs
 -----------
 
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 06/10] receive-pack: feed report options to post-receive
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (5 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 05/10] doc: add document for capability report-status-v2 Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-17 21:15                                           ` Junio C Hamano
  2020-08-15 17:17                                         ` [PATCH v17 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
                                                           ` (3 subsequent siblings)
  10 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When commands are fed to the "post-receive" hook, report options will
be parsed and the real old-oid, new-oid, reference name will feed to
the "post-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        | 29 ++++++++++++++++---
 t/t5411/once-0010-report-status-v1.sh         |  3 +-
 t/t5411/test-0032-report-with-options.sh      | 14 ++++-----
 ...est-0033-report-with-options--porcelain.sh | 14 ++++-----
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 11 +++++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 11 +++++--
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 8 files changed, 59 insertions(+), 27 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 58972aa7cf..a687218167 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -777,17 +777,38 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 {
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
+	static struct ref_push_report_options *options = NULL;
 
 	while (cmd &&
 	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
+	if (!bufp)
+		return 0; /* OK, can feed something. */
 	strbuf_reset(&state->buf);
-	strbuf_addf(&state->buf, "%s %s %s\n",
-		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
-		    cmd->ref_name);
-	state->cmd = cmd->next;
+	if (!options)
+		options = cmd->report.options;
+	if (options) {
+		struct object_id *old_oid;
+		struct object_id *new_oid;
+		const char *ref_name;
+
+		old_oid = options->old_oid ? options->old_oid : &cmd->old_oid;
+		new_oid = options->new_oid ? options->new_oid : &cmd->new_oid;
+		ref_name = options->ref_name ? options->ref_name : cmd->ref_name;
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(old_oid), oid_to_hex(new_oid),
+			    ref_name);
+		options = options->next;
+		if (!options)
+			state->cmd = cmd->next;
+	} else {
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
+			    cmd->ref_name);
+		state->cmd = cmd->next;
+	}
 	if (bufp) {
 		*bufp = state->buf.buf;
 		*sizep = state->buf.len;
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index 56a86d8ed4..bf410dc418 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -74,7 +74,8 @@ test_expect_success "proc-receive: report status v1" '
 	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
 	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
 	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/changes/23/123/1
+	post-receive< <COMMIT-B> <COMMIT-A> refs/for/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index 3742ed3c3d..b77b78c49f 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -54,7 +54,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -87,7 +87,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
@@ -121,7 +121,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
@@ -153,7 +153,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
@@ -187,7 +187,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
@@ -237,9 +237,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index f18ba9f06f..1fe352b686 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -55,7 +55,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -90,7 +90,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	Done
@@ -125,7 +125,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
@@ -158,7 +158,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
@@ -193,7 +193,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
@@ -245,9 +245,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 64662afdc6..27d58edd14 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 * [new reference] HEAD -> refs/changes/24/124/1
@@ -101,7 +103,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/24/124/1
 	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
@@ -149,7 +153,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/changes/23/123/1
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index a7e29af0c2..77b5b22ed4 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	*    HEAD:refs/changes/24/124/1    [new reference]
@@ -102,7 +104,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/24/124/1    [new reference]
 	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
@@ -151,7 +155,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	*    HEAD:refs/changes/23/123/1    [new reference]
 	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 54bec4c026..a74a2cb449 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 605b3cb5c9..e4baa13ea3 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
 	*    HEAD:refs/heads/bar    [new branch]
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 07/10] receive-pack: new config receive.procReceiveRefs
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (6 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 06/10] receive-pack: feed report options to post-receive Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-17 21:25                                           ` Junio C Hamano
  2020-08-15 17:17                                         ` [PATCH v17 08/10] t5411: test updates of remote-tracking branches Jiang Xin
                                                           ` (2 subsequent siblings)
  10 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings given by the config variables match the
reference names of the commands which are sent from git client to
`receive-pack`, these commands will be executed by an external hook
(named "proc-receive"), instead of the internal `execute_commands`
function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Optional modifiers can be provided in the beginning of value of the
config variable to filter commands for specific actions: create (a),
mofify (m), delete (d). E.g.:

    git config --system --add receive.procReceiveRefs ad:refs/heads

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt              |  19 +++
 builtin/receive-pack.c                        | 110 ++++++++++++++-
 t/t5411/once-0010-report-status-v1.sh         |   4 +
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0040-process-all-refs.sh         | 113 ++++++++++++++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 ++++++++++++++++
 t/t5411/test-0050-proc-receive-refs.sh        | 128 ++++++++++++++++++
 7 files changed, 488 insertions(+), 7 deletions(-)
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh
 create mode 100644 t/t5411/test-0050-proc-receive-refs.sh

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..7f1a1bd00c 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,25 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
++
+For example, if this variable is set to "refs/for", pushing to reference
+such as "refs/for/master" will not create or update a reference named
+"refs/for/master", but may create or update a pull request directly by
+running the hook "proc-receive".
++
+Optional modifiers can be provided in the beginning of the value to filter
+commands for specific actions: create (a), mofify (m), delete (d). E.g.:
++
+	git config --system --add receive.procReceiveRefs ad:refs/heads
+ 
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a687218167..b7e4ee1483 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -98,6 +98,16 @@ static int keepalive_in_sec = 5;
 
 static struct tmp_objdir *tmp_objdir;
 
+static struct proc_receive_ref {
+	unsigned int want_add:1,
+		     want_delete:1,
+		     want_modify:1;
+	char *ref_prefix;
+	struct proc_receive_ref *next;
+} *proc_receive_ref = NULL;
+
+static void proc_receive_ref_append(const char *prefix);
+
 static enum deny_action parse_deny_action(const char *var, const char *value)
 {
 	if (value) {
@@ -230,6 +240,13 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		if (!value)
+			return config_error_nonbool(var);
+		proc_receive_ref_append(value);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -325,6 +342,84 @@ struct command {
 	char ref_name[FLEX_ARRAY]; /* more */
 };
 
+static void proc_receive_ref_append(const char *prefix)
+{
+	struct proc_receive_ref *ref_pattern;
+	char *p;
+	int len;
+
+	ref_pattern = xcalloc(1, sizeof(struct proc_receive_ref));
+	p = strchr(prefix, ':');
+	if (p) {
+		while (prefix < p) {
+			if (*prefix == 'a')
+				ref_pattern->want_add = 1;
+			else if (*prefix == 'd')
+				ref_pattern->want_delete = 1;
+			else if (*prefix == 'm')
+				ref_pattern->want_modify = 1;
+			prefix++;
+		}
+		prefix++;
+	} else {
+		ref_pattern->want_add = 1;
+		ref_pattern->want_delete = 1;
+		ref_pattern->want_modify = 1;
+	}
+	ref_pattern->next = NULL;
+	ref_pattern->ref_prefix = xstrdup(prefix);
+	len = strlen(ref_pattern->ref_prefix);
+	while (len && ref_pattern->ref_prefix[len - 1] == '/')
+		ref_pattern->ref_prefix[--len] = '\0';
+	if (proc_receive_ref == NULL) {
+		proc_receive_ref = ref_pattern;
+	} else {
+		struct proc_receive_ref *end;
+
+		end = proc_receive_ref;
+		while (end->next)
+			end = end->next;
+		end->next = ref_pattern;
+	}
+}
+
+static int proc_receive_ref_matches(struct command *cmd)
+{
+	struct proc_receive_ref *p;
+
+	if (!proc_receive_ref)
+		return 0;
+
+	for (p = proc_receive_ref; p; p = p->next) {
+		const char *match = p->ref_prefix;
+		int neg = 0;
+		const char *remains;
+
+		if (!p->want_add && is_null_oid(&cmd->old_oid))
+			continue;
+		else if (!p->want_delete && is_null_oid(&cmd->new_oid))
+			continue;
+		else if (!p->want_modify &&
+			 !is_null_oid(&cmd->old_oid) &&
+			 !is_null_oid(&cmd->new_oid))
+			continue;
+
+		if (*match == '!') {
+			neg = 1;
+			match++;
+		}
+
+		if (skip_prefix(cmd->ref_name, match, &remains) &&
+		    (!*remains || *remains == '/')) {
+			if (!neg)
+				return 1;
+		} else if (neg) {
+			return 1;
+		}
+	}
+	return 0;
+}
+
 static void rp_error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 static void rp_warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 
@@ -1812,14 +1907,15 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_ref) {
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+			if (proc_receive_ref_matches(cmd)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
 	}
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index bf410dc418..dc2cf4a522 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -1,3 +1,7 @@
+test_expect_success "setup receive.procReceiveRefs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/for
+'
+
 test_expect_success "setup proc-receive hook" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
diff --git a/t/t5411/test-0010-proc-receive-settings.sh b/t/t5411/test-0010-proc-receive-settings.sh
new file mode 100644
index 0000000000..a36809927b
--- /dev/null
+++ b/t/t5411/test-0010-proc-receive-settings.sh
@@ -0,0 +1,7 @@
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
new file mode 100644
index 0000000000..b07c999f53
--- /dev/null
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -0,0 +1,113 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
new file mode 100644
index 0000000000..0dd9824616
--- /dev/null
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -0,0 +1,114 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+	-    :refs/heads/foo    [deleted]
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0050-proc-receive-refs.sh b/t/t5411/test-0050-proc-receive-refs.sh
new file mode 100644
index 0000000000..50d4f159f8
--- /dev/null
+++ b/t/t5411/test-0050-proc-receive-refs.sh
@@ -0,0 +1,128 @@
+test_expect_success "config receive.procReceiveRefs with modifiers ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git config --unset-all receive.procReceiveRefs &&
+		git config --add receive.procReceiveRefs m:refs/heads/master &&
+		git config --add receive.procReceiveRefs ad:refs/heads &&
+		git config --add receive.procReceiveRefs "a:!refs/heads"
+	)
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/tags/v123 " \
+		-r "option refname refs/pull/124/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : master(B)  tags/v123
+test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		v123 >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/tags/v123
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> refs/pull/123/head
+	 * [new reference] v123 -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $ZERO_OID" \
+		-r "ok refs/heads/next" \
+		-r "option refname refs/pull/124/head" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)  tags/v123
+# Refs of workbench: master(A)  tags/v123
+# git push         : NULL       NULL       next(A)
+test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOCOL)" '
+	git -C workbench push origin \
+		:refs/heads/master \
+		$A:refs/heads/next \
+		:refs/tags/v123 >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
+	remote: pre-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <ZERO-OID>
+	remote: proc-receive> ok refs/heads/next
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/pull/123/head
+	remote: post-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 - [deleted] refs/pull/123/head
+	 - [deleted] v123
+	 * [new reference] <COMMIT-A> -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 08/10] t5411: test updates of remote-tracking branches
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (7 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 09/10] transport: parse report options for tracking refs Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 10/10] doc: add documentation for the proc-receive hook Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

In order to test update of remote-tracking branches for special refs,
add new "remote.origin.fetch" settings and test cases.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 48 +++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 27d58edd14..8eec3a1ed8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -1,3 +1,18 @@
+test_expect_success "setup git config for remote-tracking of special refs" '
+	(
+		cd workbench &&
+		if ! git config --get-all remote.origin.fetch | grep refs/for/
+		then
+			git config --add remote.origin.fetch \
+				"+refs/for/*:refs/t/for/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/pull/*:refs/t/pull/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/changes/*:refs/t/changes/*"
+		fi
+	)
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -58,6 +73,17 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -120,6 +146,17 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -167,3 +204,14 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	EOF
 	test_cmp expect actual
 '
+
+test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 09/10] transport: parse report options for tracking refs
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (8 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 08/10] t5411: test updates of remote-tracking branches Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  2020-08-15 17:17                                         ` [PATCH v17 10/10] doc: add documentation for the proc-receive hook Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a pseudo reference (such as "refs/for/master/topic"), may
create or update one or more references.  The real names of the
references will be stored in the report options.  Parse report options
to create or update remote-tracking branches properly.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 22 +++++++---
 transport.c                                   | 40 ++++++++++++++-----
 2 files changed, 46 insertions(+), 16 deletions(-)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 8eec3a1ed8..73283d81e8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -78,10 +78,14 @@ test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-B> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
@@ -151,10 +155,14 @@ test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-A> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
@@ -210,8 +218,10 @@ test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/23/123/1
+	<COMMIT-B> refs/t/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/2
 '
diff --git a/transport.c b/transport.c
index 10f7dbbcd0..fe0c73fa88 100644
--- a/transport.c
+++ b/transport.c
@@ -436,28 +436,48 @@ int transport_refs_pushed(struct ref *ref)
 	return 0;
 }
 
-void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+static void update_tracking_ref(struct remote *remote, char *refname,
+				struct object_id *new_oid, int deletion,
+				int verbose)
 {
 	struct refspec_item rs;
 
-	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
-		return;
-
-	rs.src = ref->name;
+	rs.src = refname;
 	rs.dst = NULL;
-
 	if (!remote_find_tracking(remote, &rs)) {
 		if (verbose)
 			fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst);
-		if (ref->deletion) {
+		if (deletion)
 			delete_ref(NULL, rs.dst, NULL, 0);
-		} else
-			update_ref("update by push", rs.dst, &ref->new_oid,
-				   NULL, 0, 0);
+		else
+			update_ref("update by push", rs.dst, new_oid,
+				NULL, 0, 0);
 		free(rs.dst);
 	}
 }
 
+void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+{
+	char *refname;
+	struct object_id *new_oid;
+	struct ref_push_report_options *options;
+
+	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
+		return;
+
+	options = ref->report.options;
+	if (!options)
+		update_tracking_ref(remote, ref->name, &ref->new_oid,
+				    ref->deletion, verbose);
+	else
+		for (; options; options = options->next) {
+			refname = options->ref_name ? (char *)options->ref_name : ref->name;
+			new_oid = options->new_oid ? options->new_oid : &ref->new_oid;
+			update_tracking_ref(remote, refname, new_oid,
+					    is_null_oid(new_oid), verbose);
+		}
+}
+
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     struct ref_push_report_options *options,
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v17 10/10] doc: add documentation for the proc-receive hook
  2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
                                                           ` (9 preceding siblings ...)
  2020-08-15 17:17                                         ` [PATCH v17 09/10] transport: parse report options for tracking refs Jiang Xin
@ 2020-08-15 17:17                                         ` Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-15 17:17 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

"git receive-pack" that accepts requests by "git push" learned to
outsource some of the ref updates to the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 62 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 31b601e4bc..ff2f2f1fb3 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,68 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option fall-through)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status can be given in options
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option refname <refname>)
+    H: PKT-LINE(option old-oid <old-oid>)
+    H: PKT-LINE(option new-oid <new-oid>)
+    H: PKT-LINE(option forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook will use
+"option" directives to report extended attributes for the reference given
+by the above "ok/ng" directive.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.28.0


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v17 03/10] receive-pack: add new proc-receive hook
  2020-08-15 17:17                                         ` [PATCH v17 03/10] receive-pack: add new " Jiang Xin
@ 2020-08-17 20:53                                           ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-08-17 20:53 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

>
> Suggested-by: Junio C Hamano <gitster@pobox.com>

Did I????  I do not think I deserve any credit here.

> Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>


> +static struct command *find_command_by_refname(const struct command *list,
> +					       const char *refname)
> +{
> +	for (; list; list = list->next)
> +		if (!strcmp(list->ref_name, refname))
> +			return (struct command *)list;
> +	return NULL;
> +}

Does the last cast really needed, perhaps the caller is adding
"const" to its objects where it is not needed, or the variable
caller uses to receive the returned value of too loose a type?


> diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
> new file mode 100644
> index 0000000000..4f352a7baf
> --- /dev/null
> +++ b/t/helper/test-proc-receive.c
> @@ -0,0 +1,176 @@
> +#include "cache.h"
> +#include "connect.h"
> +#include "parse-options.h"
> +#include "pkt-line.h"
> +#include "sigchain.h"
> +#include "test-tool.h"
> +
> +static const char *proc_receive_usage[] = {
> +	"test-tool proc-receive [<options>...]",
> +	NULL
> +};
> +
> +static int die_version = 0;
> +static int die_readline = 0;
> +static int no_push_options = 0;
> +static int use_atomic = 0;
> +static int use_push_options = 0;
> +static int verbose = 0;

Let BSS do its job by omitting " = 0;" and " = NULL;" at the end.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v17 04/10] New capability "report-status-v2" for git-push
  2020-08-15 17:17                                         ` [PATCH v17 04/10] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-08-17 21:12                                           ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-08-17 21:12 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin, Jeff King

Jiang Xin <worldhello.net@gmail.com> writes:

> +static void report_v2(struct command *commands, const char *unpack_status)
> +{
> +	struct command *cmd;
> +	struct strbuf buf = STRBUF_INIT;
> +	struct ref_push_report_options *options;
> +
> +	packet_buf_write(&buf, "unpack %s\n",
> +			 unpack_status ? unpack_status : "ok");
> +	for (cmd = commands; cmd; cmd = cmd->next) {
> +		int count = 0;
> +
> +		if (!cmd->report.error_message)
> +			packet_buf_write(&buf, "ok %s\n",
> +					 cmd->ref_name);
> +		else
> +			packet_buf_write(&buf, "ng %s %s\n",
> +					 cmd->ref_name,
> +					 cmd->report.error_message);
> +		for (options = cmd->report.options; options; options = options->next) {
> +			if (count++ > 0)
> +				packet_buf_write(&buf, "ok %s\n", cmd->ref_name);

If there was an error, the first one gets "ng" but all others say it
was "ok"?  That smells somewhat strange.

> +			if (options->ref_name)
> +				packet_buf_write(&buf, "option refname %s\n",
> +						 options->ref_name);
> +			if (options->old_oid)
> +				packet_buf_write(&buf, "option old-oid %s\n",
> +						 oid_to_hex(options->old_oid));
> +			if (options->new_oid)
> +				packet_buf_write(&buf, "option new-oid %s\n",
> +						 oid_to_hex(options->new_oid));
> +			if (options->forced_update)
> +				packet_buf_write(&buf, "option forced-update\n");
> +		}

> +		if (ref->report.error_message)
> +			msg = ref->report.error_message;
>  		if (msg) {
>  			strbuf_addch(&buf, ' ');
>  			quote_two_c_style(&buf, "", msg, 0);
>  		}
>  		strbuf_addch(&buf, '\n');
>  
> +		for (options = ref->report.options; options; options = options->next) {
> +			if (count++ > 0)
> +				strbuf_addf(&buf, "ok %s\n", ref->name);

Would this one ever report a "ng" for an error?

> +			if (options->ref_name)
> +				strbuf_addf(&buf, "option refname %s\n",
> +					    options->ref_name);
> +			if (options->old_oid)
> +				strbuf_addf(&buf, "option old-oid %s\n",
> +					    oid_to_hex(options->old_oid));
> +			if (options->new_oid)
> +				strbuf_addf(&buf, "option new-oid %s\n",
> +					    oid_to_hex(options->new_oid));
> +			if (options->forced_update)
> +				strbuf_addstr(&buf, "option forced-update\n");
> +		}
>  		write_or_die(1, buf.buf, buf.len);
>  	}
>  	strbuf_release(&buf);

> diff --git a/transport-helper.c b/transport-helper.c
> index defafbf4c1..0029ba18bd 100644
> --- a/transport-helper.c
> +++ b/transport-helper.c
> @@ -729,6 +729,49 @@ static int push_update_ref_status(struct strbuf *buf,
>  {
>  	char *refname, *msg;
>  	int status, forced = 0;
> +	static struct ref *hint = NULL;
> +	static int new_options = 1;

I hate to see more helper functions turned into pieces that are
impossible to reuse by gaining internal state like this.  Does this
function have so many callers that makes it impractical to update
its signature to have the pointer to a "state" structure passed by
the caller?  This is called only from push_update_refs_status(), so
you should be able to define a new structure that has the two fields

	struct {
		struct ref *hint;
		int new_option;
	};

initialize an instance of it before push_update_refs_status() enters
its endless loop, and pass a pointer to it to this helper function.

> +	if (starts_with(buf->buf, "option ")) {
> +		struct ref_push_report_options *options;
> +		struct object_id old_oid, new_oid;
> +		const char *key, *val;
> +		char *p;
> +
> +		if (!hint)
> +			die(_("'option' without a matching 'ok/error' directive"));
> +		options = hint->report.options;
> +		while (options && options->next)
> +			options = options->next;
> +		if (new_options) {
> +			if (!options) {
> +				hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
> +				options = hint->report.options;
> +			} else {
> +				options->next = xcalloc(1, sizeof(struct ref_push_report_options));
> +				options = options->next;
> +			}
> +			new_options = 0;
> +		}


> -static int print_one_push_status(struct ref *ref, const char *dest, int count,
> -				 int porcelain, int summary_width)
> +static int _print_one_push_status(struct ref *ref, const char *dest, int count,
> +				  struct ref_push_report_options *options,
> +				  int porcelain, int summary_width)

Our naming convention avoids leading underscore (instead we use _1
suffix, when we cannot come up with a better name).

> +static int print_one_push_status(struct ref *ref, const char *dest, int count,
> +				 int porcelain, int summary_width)
> +{
> +	struct ref_push_report_options *options;
> +	int n = 0;
> +
> +	if (!ref->report.options)
> +		return _print_one_push_status(ref, dest, count,
> +					      NULL, porcelain, summary_width);
> +
> +	for (options = ref->report.options; options; options = options->next)

This comment applies to all the patches in this series that adds the
same pattern, but 

 * "ref->report.options" may be a good name in that it is a (linked)
   list of options;

 * The variable that is used to iterate over the list points at one
   of the elements on the list at any time, and is better called
   'option' without 's' at the end.

> +		_print_one_push_status(ref, dest, count + n++,
> +				       options, porcelain, summary_width);
> +	return n;
> +}
> +
>  static int measure_abbrev(const struct object_id *oid, int sofar)
>  {
>  	char hex[GIT_MAX_HEXSZ + 1];

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v17 06/10] receive-pack: feed report options to post-receive
  2020-08-15 17:17                                         ` [PATCH v17 06/10] receive-pack: feed report options to post-receive Jiang Xin
@ 2020-08-17 21:15                                           ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-08-17 21:15 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index 58972aa7cf..a687218167 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -777,17 +777,38 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
>  {
>  	struct receive_hook_feed_state *state = state_;
>  	struct command *cmd = state->cmd;
> +	static struct ref_push_report_options *options = NULL;

Exactly the same comment as "don't turn helper functions that can be
reused into unreusable ones by adding internal states" as one of the
previous patches applies here.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v17 07/10] receive-pack: new config receive.procReceiveRefs
  2020-08-15 17:17                                         ` [PATCH v17 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-08-17 21:25                                           ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-08-17 21:25 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index a687218167..b7e4ee1483 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -98,6 +98,16 @@ static int keepalive_in_sec = 5;
>  
>  static struct tmp_objdir *tmp_objdir;
>  
> +static struct proc_receive_ref {
> +	unsigned int want_add:1,
> +		     want_delete:1,
> +		     want_modify:1;
> +	char *ref_prefix;
> +	struct proc_receive_ref *next;
> +} *proc_receive_ref = NULL;

Let BSS do its job here.

> +static void proc_receive_ref_append(const char *prefix);
> +
>  static enum deny_action parse_deny_action(const char *var, const char *value)
>  {
>  	if (value) {
> @@ -230,6 +240,13 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
>  		return 0;
>  	}
>  
> +	if (strcmp(var, "receive.procreceiverefs") == 0) {

We may want to fix the style of this function in a preliminary clean-up,
but not in the middle of the series, so let's agree to let this pass.

> @@ -325,6 +342,84 @@ struct command {
>  	char ref_name[FLEX_ARRAY]; /* more */
>  };
>  
> +static void proc_receive_ref_append(const char *prefix)
> +{
> +	struct proc_receive_ref *ref_pattern;
> +	char *p;
> +	int len;
> +
> +	ref_pattern = xcalloc(1, sizeof(struct proc_receive_ref));
> +	p = strchr(prefix, ':');

A colon ':' is not a valid character in a refname, so the use of it
as a separator for the prefix would be reasonable.

> +	if (p) {
> +		while (prefix < p) {
> +			if (*prefix == 'a')
> +				ref_pattern->want_add = 1;
> +			else if (*prefix == 'd')
> +				ref_pattern->want_delete = 1;
> +			else if (*prefix == 'm')
> +				ref_pattern->want_modify = 1;
> +			prefix++;
> +		}
> +		prefix++;
> +	} else {
> +		ref_pattern->want_add = 1;
> +		ref_pattern->want_delete = 1;
> +		ref_pattern->want_modify = 1;
> +	}
> +	ref_pattern->next = NULL;


> +	ref_pattern->ref_prefix = xstrdup(prefix);
> +	len = strlen(ref_pattern->ref_prefix);
> +	while (len && ref_pattern->ref_prefix[len - 1] == '/')
> +		ref_pattern->ref_prefix[--len] = '\0';

Why not count "size_t len" before you make a copy of prefix with
tail adjustment, so that you can do xmemdupz() at the end without
need for potential overallocation? 

> +	if (proc_receive_ref == NULL) {

Style:
	if (!proc_receive_ref) {

like you did below.

> +static int proc_receive_ref_matches(struct command *cmd)
> +{
> +	struct proc_receive_ref *p;
> +
> +	if (!proc_receive_ref)
> +		return 0;
> +
> +	for (p = proc_receive_ref; p; p = p->next) {
> +		const char *match = p->ref_prefix;
> +		int neg = 0;
> +		const char *remains;
> +
> +		if (!p->want_add && is_null_oid(&cmd->old_oid))
> +			continue;
> +		else if (!p->want_delete && is_null_oid(&cmd->new_oid))
> +			continue;
> +		else if (!p->want_modify &&
> +			 !is_null_oid(&cmd->old_oid) &&
> +			 !is_null_oid(&cmd->new_oid))
> +			continue;
> +
> +		if (*match == '!') {
> +			neg = 1;
> +			match++;
> +		}

Shouldn't the "negate" bit be jsut another field that has
want_{add,delete,modify}, so that proc_receive_ref_append()
can parse it only once without these four lines?


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v18 00/10] New proc-receive hook for centralized workflow
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 " Jiang Xin
                                                               ` (10 more replies)
  2020-08-24 17:41                                           ` [PATCH v18 01/10] transport: not report a non-head push as a branch Jiang Xin
                                                             ` (9 subsequent siblings)
  10 siblings, 11 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v17

Fixed all issues found in review on v17.

> Did I????  I do not think I deserve any credit here.

    @@ Commit message
         extended in latter commit just like what the "proc-receive" hook reports
         to "receive-pack".
     
    -    Suggested-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>


> Does the last cast really needed, perhaps the caller is adding
> "const" to its objects where it is not needed, or the variable
> caller uses to receive the returned value of too loose a type?

     @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
      	return finish_command(&proc);
      }
      
    -+static struct command *find_command_by_refname(const struct command *list,
    ++static struct command *find_command_by_refname(struct command *list,
     +					       const char *refname)
     +{
     +	for (; list; list = list->next)
     +		if (!strcmp(list->ref_name, refname))
    -+			return (struct command *)list;
    ++			return list;
     +	return NULL;
     +}
     +

> Let BSS do its job by omitting " = 0;" and " = NULL;" at the end.

    @@ t/helper/test-proc-receive.c (new)
     +	NULL
     +};
     +
    -+static int die_version = 0;
    -+static int die_readline = 0;
    -+static int no_push_options = 0;
    -+static int use_atomic = 0;
    -+static int use_push_options = 0;
    -+static int verbose = 0;
    ++static int die_version;
    ++static int die_readline;
    ++static int no_push_options;
    ++static int use_atomic;
    ++static int use_push_options;
    ++static int verbose;
     +static int version = 1;
     +static struct string_list returns = STRING_LIST_INIT_NODUP;
     +

> This comment applies to all the patches in this series that adds the
> same pattern, but 
> 
>  * "ref->report.options" may be a good name in that it is a (linked)
>    list of options;
> 
>  * The variable that is used to iterate over the list points at one
>    of the elements on the list at any time, and is better called
>    'option' without 's' at the end.

Merge "struct ref_push_report_options" into "struct ref_push_report".

     @@ remote.h: int for_each_remote(each_remote_fn fn, void *priv);
      
      int remote_has_url(struct remote *remote, const char *url);
      
    -+struct ref_push_report_options {
    ++struct ref_push_report {
     +	const char *ref_name;
     +	struct object_id *old_oid;
     +	struct object_id *new_oid;
     +	unsigned int forced_update:1;
    -+	struct ref_push_report_options *next;
    -+};
    -+
    -+struct ref_push_report {
    -+	const char *message;
    -+	const char *error_message;
    -+	struct ref_push_report_options *options;
    ++	struct ref_push_report *next;
     +};
     +
      struct ref {

> If there was an error, the first one gets "ng" but all others say it
> was "ok"?  That smells somewhat strange.

> Would this one ever report a "ng" for an error?

Preserve "error_string" in command, and no "options" for an "ng" error.

    @@ builtin/receive-pack.c: static void write_head_info(void)
     +#define RUN_PROC_RECEIVE_RETURNED	2
      struct command {
      	struct command *next;
    --	const char *error_string;
    -+	struct ref_push_report report;
    + 	const char *error_string;
    ++	struct ref_push_report *report;
      	unsigned int skip_update:1,
     -		     did_not_exist:1;
     +		     did_not_exist:1,


    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +{
     +	struct command *cmd;
     +	struct command *hint = NULL;
    ++	struct ref_push_report *report = NULL;
    ++	int new_report = 0;
     +	int code = 0;
    -+	int new_options = 1;
    ++	int once = 0;
     +
     +	for (;;) {
     +		struct object_id old_oid, new_oid;
    @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd)
     +		}
     +		*p++ = '\0';
     +		if (!strcmp(head, "option")) {
    -+			struct ref_push_report_options *options;
     +			const char *key, *val;
     +
    -+			if (!hint) {
    -+			       if (new_options) {
    ++			if (!hint || !(report || new_report)) {
    ++				if (!once++)
     +					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
    -+					new_options = 0;
    -+				}
     +				code = -1;
     +				continue;
     +			}
    -+			options = hint->report.options;
    -+			while (options && options->next)
    -+				options = options->next;
    -+			if (new_options) {
    -+				if (!options) {
    -+					hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
    -+					options = hint->report.options;
    ++			if (new_report) {
    ++				if (!hint->report) {
    ++					hint->report = xcalloc(1, sizeof(struct ref_push_report));
    ++					report = hint->report;
     +				} else {
    -+					options->next = xcalloc(1, sizeof(struct ref_push_report_options));
    -+					options = options->next;
    ++					report = hint->report;
    ++					while (report->next)
    ++						report = report->next;
    ++					report->next = xcalloc(1, sizeof(struct ref_push_report));
    ++					report = report->next;
     +				}
    -+				new_options = 0;
    ++				new_report = 0;
     +			}
    -+			assert(options);
     +			key = p;
     +			p = strchr(key, ' ');
     +			if (p)
     +				*p++ = '\0';
     +			val = p;
     +			if (!strcmp(key, "refname"))
    -+				options->ref_name = xstrdup_or_null(val);
    ++				report->ref_name = xstrdup_or_null(val);
     +			else if (!strcmp(key, "old-oid") && val &&
     +				 !parse_oid_hex(val, &old_oid, &val))
    -+				options->old_oid = oiddup(&old_oid);
    ++				report->old_oid = oiddup(&old_oid);
     +			else if (!strcmp(key, "new-oid") && val &&
     +				 !parse_oid_hex(val, &new_oid, &val))
    -+				options->new_oid = oiddup(&new_oid);
    ++				report->new_oid = oiddup(&new_oid);
     +			else if (!strcmp(key, "forced-update"))
    -+				options->forced_update = 1;
    ++				report->forced_update = 1;
     +			else if (!strcmp(key, "fall-through"))
     +				/* Fall through, let 'receive-pack' to execute it. */
     +				hint->run_proc_receive = 0;
     +			continue;
     +		}
     +
    ++		report = NULL;
    ++		new_report = 0;
     +		refname = p;
     +		p = strchr(refname, ' ');
     +		if (p)

> Our naming convention avoids leading underscore (instead we use _1
> suffix, when we cannot come up with a better name).

Rename "_print_one_push_status" to "print_one_push_report".

    @@ transport.c: static void print_ok_ref_status(struct ref *ref, int porcelain, int
      
      		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
     -				 porcelain, summary_width);
    -+				 options, porcelain, summary_width);
    ++				 report, porcelain, summary_width);
      		strbuf_release(&quickref);
      	}
      }
      
     -static int print_one_push_status(struct ref *ref, const char *dest, int count,
    --				 int porcelain, int summary_width)
    -+static int _print_one_push_status(struct ref *ref, const char *dest, int count,
    -+				  struct ref_push_report_options *options,
    -+				  int porcelain, int summary_width)
    ++static int print_one_push_report(struct ref *ref, const char *dest, int count,
    ++				 struct ref_push_report *report,
    + 				 int porcelain, int summary_width)
      {
      	if (!count) {
    - 		char *url = transport_anonymize_url(dest);


> I hate to see more helper functions turned into pieces that are
> impossible to reuse by gaining internal state like this.  Does this
> function have so many callers that makes it impractical to update
> its signature to have the pointer to a "state" structure passed by
> the caller?  This is called only from push_update_refs_status(), so
> you should be able to define a new structure that has the two fields
> 
> 	struct {
> 		struct ref *hint;
> 		int new_option;
> 	};
> 
> initialize an instance of it before push_update_refs_status() enters
> its endless loop, and pass a pointer to it to this helper function.

      ## transport-helper.c ##
    -@@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
    +@@ transport-helper.c: static int fetch(struct transport *transport,
    + 	return -1;
    + }
    + 
    ++struct push_update_ref_state {
    ++	struct ref *hint;
    ++	struct ref_push_report *report;
    ++	int new_report;
    ++};
    ++
    + static int push_update_ref_status(struct strbuf *buf,
    +-				   struct ref **ref,
    ++				   struct push_update_ref_state *state,
    + 				   struct ref *remote_refs)
      {
      	char *refname, *msg;
      	int status, forced = 0;
    -+	static struct ref *hint = NULL;
    -+	static int new_options = 1;
    -+
    + 
     +	if (starts_with(buf->buf, "option ")) {
    -+		struct ref_push_report_options *options;
     +		struct object_id old_oid, new_oid;
     +		const char *key, *val;
     +		char *p;
     +
    -+		if (!hint)
    ++		if (!state->hint || !(state->report || state->new_report))
     +			die(_("'option' without a matching 'ok/error' directive"));
    -+		options = hint->report.options;
    -+		while (options && options->next)
    -+			options = options->next;
    -+		if (new_options) {
    -+			if (!options) {
    -+				hint->report.options = xcalloc(1, sizeof(struct ref_push_report_options));
    -+				options = hint->report.options;
    ++		if (state->new_report) {
    ++			if (!state->hint->report) {
    ++				state->hint->report = xcalloc(1, sizeof(struct ref_push_report));
    ++				state->report = state->hint->report;
     +			} else {
    -+				options->next = xcalloc(1, sizeof(struct ref_push_report_options));
    -+				options = options->next;
    ++				state->report = state->hint->report;
    ++				while (state->report->next)
    ++					state->report = state->report->next;
    ++				state->report->next = xcalloc(1, sizeof(struct ref_push_report));
    ++				state->report = state->report->next;
     +			}
    -+			new_options = 0;
    ++			state->new_report = 0;
     +		}
    -+		assert(options);
     +		key = buf->buf + 7;
     +		p = strchr(key, ' ');
     +		if (p)
     +			*p++ = '\0';
     +		val = p;
     +		if (!strcmp(key, "refname"))
    -+			options->ref_name = xstrdup_or_null(val);
    ++			state->report->ref_name = xstrdup_or_null(val);
     +		else if (!strcmp(key, "old-oid") && val &&
     +			 !parse_oid_hex(val, &old_oid, &val))
    -+			options->old_oid = oiddup(&old_oid);
    ++			state->report->old_oid = oiddup(&old_oid);
     +		else if (!strcmp(key, "new-oid") && val &&
     +			 !parse_oid_hex(val, &new_oid, &val))
    -+			options->new_oid = oiddup(&new_oid);
    ++			state->report->new_oid = oiddup(&new_oid);
     +		else if (!strcmp(key, "forced-update"))
    -+			options->forced_update = 1;
    ++			state->report->forced_update = 1;
     +		/* Not update remote namespace again. */
     +		return 1;
     +	}
    - 
    ++
    ++	state->report = NULL;
    ++	state->new_report = 0;
    ++
      	if (starts_with(buf->buf, "ok ")) {
      		status = REF_STATUS_OK;
    + 		refname = buf->buf + 3;
     @@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
    - 		*ref = find_ref_by_name(remote_refs, refname);
    - 	if (!*ref) {
    + 		}
    + 	}
    + 
    +-	if (*ref)
    +-		*ref = find_ref_by_name(*ref, refname);
    +-	if (!*ref)
    +-		*ref = find_ref_by_name(remote_refs, refname);
    +-	if (!*ref) {
    ++	if (state->hint)
    ++		state->hint = find_ref_by_name(state->hint, refname);
    ++	if (!state->hint)
    ++		state->hint = find_ref_by_name(remote_refs, refname);
    ++	if (!state->hint) {
      		warning(_("helper reported unexpected status of %s"), refname);
    -+		hint = NULL;
      		return 1;
      	}
    -+	hint = *ref;
    -+	new_options = 1;
      
    - 	if ((*ref)->status != REF_STATUS_NONE) {
    +-	if ((*ref)->status != REF_STATUS_NONE) {
    ++	if (state->hint->status != REF_STATUS_NONE) {
      		/*
    + 		 * Earlier, the ref was marked not to be pushed, so ignore the ref
    + 		 * status reported by the remote helper if the latter is 'no match'.
     @@ transport-helper.c: static int push_update_ref_status(struct strbuf *buf,
    + 			return 1;
    + 	}
      
    - 	(*ref)->status = status;
    - 	(*ref)->forced_update |= forced;
    +-	(*ref)->status = status;
    +-	(*ref)->forced_update |= forced;
     -	(*ref)->remote_status = msg;
    -+	if (msg) {
    -+		if (status == REF_STATUS_OK)
    -+			(*ref)->report.message = msg;
    -+		else
    -+			(*ref)->report.error_message = msg;
    -+	}
    ++	if (status == REF_STATUS_OK)
    ++		state->new_report = 1;
    ++	state->hint->status = status;
    ++	state->hint->forced_update |= forced;
    ++	state->hint->remote_status = msg;
      	return !(status == REF_STATUS_OK);
      }
      
    +@@ transport-helper.c: static int push_update_refs_status(struct helper_data *data,
    + 				    struct ref *remote_refs,
    + 				    int flags)
    + {
    ++	struct ref *ref;
    ++	struct ref_push_report *report;
    + 	struct strbuf buf = STRBUF_INIT;
    +-	struct ref *ref = remote_refs;
    +-	int ret = 0;
    ++	struct push_update_ref_state state = { remote_refs, NULL, 0 };
    + 
    + 	for (;;) {
    +-		char *private;
    +-
    + 		if (recvline(data, &buf)) {
    +-			ret = 1;
    +-			break;
    ++			strbuf_release(&buf);
    ++			return 1;
    + 		}
    +-
    + 		if (!buf.len)
    + 			break;
    ++		push_update_ref_status(&buf, &state, remote_refs);
    ++	}
    ++	strbuf_release(&buf);
    + 
    +-		if (push_update_ref_status(&buf, &ref, remote_refs))
    +-			continue;
    ++	if (flags & TRANSPORT_PUSH_DRY_RUN || !data->rs.nr || data->no_private_update)
    ++		return 0;
    + 
    +-		if (flags & TRANSPORT_PUSH_DRY_RUN || !data->rs.nr || data->no_private_update)
    +-			continue;
    ++	/* propagate back the update to the remote namespace */
    ++	for (ref = remote_refs; ref; ref = ref->next) {
    ++		char *private;
    + 
    +-		/* propagate back the update to the remote namespace */
    +-		private = apply_refspecs(&data->rs, ref->name);
    +-		if (!private)
    ++		if (ref->status != REF_STATUS_OK)
    + 			continue;
    +-		update_ref("update by helper", private, &ref->new_oid, NULL,
    +-			   0, 0);
    +-		free(private);
    ++
    ++		if (!ref->report) {
    ++			private = apply_refspecs(&data->rs, ref->name);
    ++			if (!private)
    ++				continue;
    ++			update_ref("update by helper", private, &(ref->new_oid),
    ++				   NULL, 0, 0);
    ++			free(private);
    ++		} else {
    ++			for (report = ref->report; report; report = report->next) {
    ++				private = apply_refspecs(&data->rs,
    ++							 report->ref_name
    ++							 ? report->ref_name
    ++							 : ref->name);
    ++				if (!private)
    ++					continue;
    ++				update_ref("update by helper", private,
    ++					   report->new_oid
    ++					   ? report->new_oid
    ++					   : &(ref->new_oid),
    ++					   NULL, 0, 0);
    ++				free(private);
    ++			}
    ++		}
    + 	}
    +-	strbuf_release(&buf);
    +-	return ret;
    ++	return 0;
    + }
    + 
    + static void set_common_push_options(struct transport *transport,

> Exactly the same comment as "don't turn helper functions that can be
> reused into unreusable ones by adding internal states" as one of the
> previous patches applies here.


 6:  ca4fc6d511 !  4:  60b9157a8e receive-pack: feed report options to post-receive
    @@ Commit message
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## builtin/receive-pack.c ##
    -@@ builtin/receive-pack.c: static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
    - {
    - 	struct receive_hook_feed_state *state = state_;
    - 	struct command *cmd = state->cmd;
    -+	static struct ref_push_report_options *options = NULL;
    +@@ builtin/receive-pack.c: static void prepare_push_cert_sha1(struct child_process *proc)
      
    - 	while (cmd &&
    - 	       state->skip_broken && (cmd->report.error_message || cmd->did_not_exist))
    + struct receive_hook_feed_state {
    + 	struct command *cmd;
    ++	struct ref_push_report *report;
    + 	int skip_broken;
    + 	struct strbuf buf;
    + 	const struct string_list *push_options;
    +@@ builtin/receive-pack.c: static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
      		cmd = cmd->next;
      	if (!cmd)
      		return -1; /* EOF */
    @@ builtin/receive-pack.c: static int feed_receive_hook(void *state_, const char **
     -		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
     -		    cmd->ref_name);
     -	state->cmd = cmd->next;
    -+	if (!options)
    -+		options = cmd->report.options;
    -+	if (options) {
    ++	if (!state->report)
    ++		state->report = cmd->report;
    ++	if (state->report) {
     +		struct object_id *old_oid;
     +		struct object_id *new_oid;
     +		const char *ref_name;
     +
    -+		old_oid = options->old_oid ? options->old_oid : &cmd->old_oid;
    -+		new_oid = options->new_oid ? options->new_oid : &cmd->new_oid;
    -+		ref_name = options->ref_name ? options->ref_name : cmd->ref_name;
    ++		old_oid = state->report->old_oid ? state->report->old_oid : &cmd->old_oid;
    ++		new_oid = state->report->new_oid ? state->report->new_oid : &cmd->new_oid;
    ++		ref_name = state->report->ref_name ? state->report->ref_name : cmd->ref_name;
     +		strbuf_addf(&state->buf, "%s %s %s\n",
     +			    oid_to_hex(old_oid), oid_to_hex(new_oid),
     +			    ref_name);
    -+		options = options->next;
    -+		if (!options)
    ++		state->report = state->report->next;
    ++		if (!state->report)
     +			state->cmd = cmd->next;
     +	} else {
     +		strbuf_addf(&state->buf, "%s %s %s\n",

    +@@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,
    + 	strbuf_init(&state.buf, 0);
    + 	state.cmd = commands;
    + 	state.skip_broken = skip_broken;
    ++	state.report = NULL;
    + 	if (feed_receive_hook(&state, NULL, NULL))
    + 		return 0;
    + 	state.cmd = commands;
 
> Why not count "size_t len" before you make a copy of prefix with
> tail adjustment, so that you can do xmemdupz() at the end without
> need for potential overallocation? 

> Style:
> 	if (!proc_receive_ref) {
> 
> like you did below.

    @@ builtin/receive-pack.c: struct command {
     +		ref_pattern->want_delete = 1;
     +		ref_pattern->want_modify = 1;
     +	}
    -+	ref_pattern->next = NULL;
    -+	ref_pattern->ref_prefix = xstrdup(prefix);
    -+	len = strlen(ref_pattern->ref_prefix);
    -+	while (len && ref_pattern->ref_prefix[len - 1] == '/')
    -+		ref_pattern->ref_prefix[--len] = '\0';
    -+	if (proc_receive_ref == NULL) {
    ++	len = strlen(prefix);
    ++	while (len && prefix[len - 1] == '/')
    ++		len--;
    ++	ref_pattern->ref_prefix = xmemdupz(prefix, len);
    ++	if (!proc_receive_ref) {
     +		proc_receive_ref = ref_pattern;
     +	} else {
     +		struct proc_receive_ref *end;


> Shouldn't the "negate" bit be jsut another field that has
> want_{add,delete,modify}, so that proc_receive_ref_append()
> can parse it only once without these four lines?

    @@ builtin/receive-pack.c: static int keepalive_in_sec = 5;
     +static struct proc_receive_ref {
     +	unsigned int want_add:1,
     +		     want_delete:1,
    -+		     want_modify:1;
    ++		     want_modify:1,
    ++		     negative_ref:1;
     +	char *ref_prefix;
     +	struct proc_receive_ref *next;
    -+} *proc_receive_ref = NULL;
    ++} *proc_receive_ref;
     +
     +static void proc_receive_ref_append(const char *prefix);
     +
    @@ builtin/receive-pack.c: struct command {
     +				ref_pattern->want_delete = 1;
     +			else if (*prefix == 'm')
     +				ref_pattern->want_modify = 1;
    ++			else if (*prefix == '!')
    ++				ref_pattern->negative_ref = 1;
     +			prefix++;
     +		}
     +		prefix++;

    @@ builtin/receive-pack.c: struct command {
     +
     +	for (p = proc_receive_ref; p; p = p->next) {
     +		const char *match = p->ref_prefix;
    -+		int neg = 0;
     +		const char *remains;
     +
     +		if (!p->want_add && is_null_oid(&cmd->old_oid))
    @@ builtin/receive-pack.c: struct command {
     +			 !is_null_oid(&cmd->new_oid))
     +			continue;
     +
    -+		if (*match == '!') {
    -+			neg = 1;
    -+			match++;
    -+		}
    -+
     +		if (skip_prefix(cmd->ref_name, match, &remains) &&
     +		    (!*remains || *remains == '/')) {
    -+			if (!neg)
    ++			if (!p->negative_ref)
     +				return 1;
    -+		} else if (neg) {
    ++		} else if (p->negative_ref) {
     +			return 1;
     +		}
     +	}
    @@ t/t5411/test-0050-proc-receive-refs.sh (new)
     +		git config --unset-all receive.procReceiveRefs &&
     +		git config --add receive.procReceiveRefs m:refs/heads/master &&
     +		git config --add receive.procReceiveRefs ad:refs/heads &&
    -+		git config --add receive.procReceiveRefs "a:!refs/heads"
    ++		git config --add receive.procReceiveRefs "a!:refs/heads"
     +	)
     +'
     +

---

Jiang Xin (10):
  transport: not report a non-head push as a branch
  t5411: add basic test cases for proc-receive hook
  receive-pack: add new proc-receive hook
  receive-pack: feed report options to post-receive
  New capability "report-status-v2" for git-push
  doc: add document for capability report-status-v2
  receive-pack: new config receive.procReceiveRefs
  t5411: test updates of remote-tracking branches
  transport: parse report options for tracking refs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt              |  21 +
 Documentation/githooks.txt                    |  62 +++
 Documentation/technical/pack-protocol.txt     |  43 +-
 .../technical/protocol-capabilities.txt       |  17 +-
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 472 +++++++++++++++++-
 builtin/send-pack.c                           |  19 +
 remote.h                                      |   9 +
 send-pack.c                                   | 101 +++-
 t/helper/test-proc-receive.c                  | 176 +++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411-proc-receive-hook.sh                  | 117 +++++
 t/t5411/common-functions.sh                   |  56 +++
 t/t5411/once-0010-report-status-v1.sh         |  94 ++++
 t/t5411/test-0000-standard-git-push.sh        | 143 ++++++
 .../test-0001-standard-git-push--porcelain.sh | 147 ++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0011-no-hook-error.sh            |  64 +++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 +++
 t/t5411/test-0013-bad-protocol.sh             | 217 ++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++
 t/t5411/test-0020-report-ng.sh                |  67 +++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 +++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 ++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 ++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 +++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 +++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 ++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 ++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 ++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 ++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 227 +++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 172 +++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 ++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 ++++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 +++++
 t/t5411/test-0050-proc-receive-refs.sh        | 128 +++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport-helper.c                            | 128 +++--
 transport.c                                   | 159 ++++--
 48 files changed, 4320 insertions(+), 102 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/once-0010-report-status-v1.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh
 create mode 100644 t/t5411/test-0050-proc-receive-refs.sh

-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v18 01/10] transport: not report a non-head push as a branch
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
                                                             ` (8 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5516-fetch-push.sh | 2 +-
 transport.c           | 9 ++++++---
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 9c6218f568..77938db77f 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 15f5ba4e8f..26fefd13c1 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 02/10] t5411: add basic test cases for proc-receive hook
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 01/10] transport: not report a non-head push as a branch Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 03/10] receive-pack: add new " Jiang Xin
                                                             ` (7 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Topic "proc-receive-hook" will change the workflow and output of
git-push. Add some basic test cases in t5411 before introducing the new
topic.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh                  | 108 +++++++++++++
 t/t5411/common-functions.sh                   |  56 +++++++
 t/t5411/test-0000-standard-git-push.sh        | 143 +++++++++++++++++
 .../test-0001-standard-git-push--porcelain.sh | 147 ++++++++++++++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++++
 6 files changed, 521 insertions(+)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..3a684353a8
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+setup_upstream_and_workbench () {
+	# Refs of upstream : master(A)
+	# Refs of workbench: master(A)  tags/v123
+	test_expect_success "setup upstream and workbench" '
+		rm -rf upstream.git &&
+		rm -rf workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git tag -m "v123" v123 $A &&
+			git remote add origin ../upstream.git &&
+			git push origin master &&
+			git update-ref refs/heads/master $A $B &&
+			git -C ../upstream.git update-ref \
+				refs/heads/master $A $B
+		) &&
+		TAG=$(git -C workbench rev-parse v123) &&
+
+		# setup pre-receive hook
+		write_script upstream.git/hooks/pre-receive <<-\EOF &&
+		exec >&2
+		echo "# pre-receive hook"
+		while read old new ref
+		do
+			echo "pre-receive< $old $new $ref"
+		done
+		EOF
+
+		# setup post-receive hook
+		write_script upstream.git/hooks/post-receive <<-\EOF &&
+		exec >&2
+		echo "# post-receive hook"
+		while read old new ref
+		do
+			echo "post-receive< $old $new $ref"
+		done
+		EOF
+
+		upstream=upstream.git
+	'
+}
+
+run_proc_receive_hook_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	local)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Include test cases for both file and HTTP protocol
+	for t in  "$TEST_DIRECTORY"/t5411/test-*.sh
+	do
+		. "$t"
+	done
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run test cases for 'proc-receive' hook on local file protocol.
+run_proc_receive_hook_test local
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup for HTTP protocol" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin "$HTTPD_URL/auth-push/smart/upstream.git" &&
+	set_askpass user@host pass@host
+'
+
+setup_askpass_helper
+
+# Run test cases for 'proc-receive' hook on HTTP protocol.
+run_proc_receive_hook_test http
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6580bebd8e
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,56 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
new file mode 100644
index 0000000000..e206587348
--- /dev/null
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -0,0 +1,143 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> next
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [rejected] master -> master (non-fast-forward)
+	 ! [rejected] <COMMIT-B> -> next (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> next
+	 ! [rejected] master -> master (non-fast-forward)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> master -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] master -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
new file mode 100644
index 0000000000..48f6fcc846
--- /dev/null
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -0,0 +1,147 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/next    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --atomic --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "s/^# GETTEXT POISON #//" \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	! refs/heads/master:refs/heads/master [rejected] (non-fast-forward)
+	! <COMMIT-B>:refs/heads/next [rejected] (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/next    <OID-A>..<OID-B>
+	!    refs/heads/master:refs/heads/master    [rejected] (non-fast-forward)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	+    refs/heads/master:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	-    :refs/heads/next    [deleted]
+	*    refs/tags/v123:refs/tags/v123    [new tag]
+	*    refs/heads/master:refs/review/master/topic    [new reference]
+	*    HEAD:refs/heads/a/b/c    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
new file mode 100644
index 0000000000..c246f7e68e
--- /dev/null
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -0,0 +1,33 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (pre-receive hook declined)
+	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
new file mode 100644
index 0000000000..b14894de81
--- /dev/null
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (pre-receive hook declined)
+	!    HEAD:refs/heads/next    [remote rejected] (pre-receive hook declined)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 03/10] receive-pack: add new proc-receive hook
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (2 preceding siblings ...)
  2020-08-24 17:41                                           ` [PATCH v18 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 04/10] receive-pack: feed report options to post-receive Jiang Xin
                                                             ` (6 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (an hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, The letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option fall-through)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status can be given in options
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option refname <refname>)
    H: PKT-LINE(option old-oid <old-oid>)
    H: PKT-LINE(option new-oid <new-oid>)
    H: PKT-LINE(option forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in option lines.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" will be
extended in latter commit just like what the "proc-receive" hook reports
to "receive-pack".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 296 +++++++++++++++++-
 remote.h                                      |   8 +
 t/helper/test-proc-receive.c                  | 176 +++++++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411/test-0011-no-hook-error.sh            |  64 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 ++++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++++++
 t/t5411/test-0020-report-ng.sh                |  67 ++++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 ++++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 +++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 +++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 +++
 t/t5411/test-0026-push-options.sh             |  79 +++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 +++++
 t/t5411/test-0030-report-ok.sh                |  35 +++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 +++
 t/t5411/test-0032-report-with-options.sh      | 256 +++++++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 ++++++++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 +++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 +++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 159 ++++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 162 ++++++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 ++++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 ++++++
 28 files changed, 2626 insertions(+), 3 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh

diff --git a/Makefile b/Makefile
index 3d3a39fc19..607aeabeee 100644
--- a/Makefile
+++ b/Makefile
@@ -722,6 +722,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a00f91c1a0..2b63b92545 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -308,11 +308,15 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
+	struct ref_push_report *report;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -838,6 +842,268 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	struct ref_push_report *report = NULL;
+	int new_report = 0;
+	int code = 0;
+	int once = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *head;
+		const char *refname;
+		char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head);
+			code = -1;
+			continue;
+		}
+		*p++ = '\0';
+		if (!strcmp(head, "option")) {
+			const char *key, *val;
+
+			if (!hint || !(report || new_report)) {
+				if (!once++)
+					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
+				code = -1;
+				continue;
+			}
+			if (new_report) {
+				if (!hint->report) {
+					hint->report = xcalloc(1, sizeof(struct ref_push_report));
+					report = hint->report;
+				} else {
+					report = hint->report;
+					while (report->next)
+						report = report->next;
+					report->next = xcalloc(1, sizeof(struct ref_push_report));
+					report = report->next;
+				}
+				new_report = 0;
+			}
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				report->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				report->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				report->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				report->forced_update = 1;
+			else if (!strcmp(key, "fall-through"))
+				/* Fall through, let 'receive-pack' to execute it. */
+				hint->run_proc_receive = 0;
+			continue;
+		}
+
+		report = NULL;
+		new_report = 0;
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    head, refname);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		if (!strcmp(head, "ng")) {
+			if (p)
+				hint->error_string = xstrdup(p);
+			else
+				hint->error_string = "failed";
+			code = -1;
+			continue;
+		}
+		new_report = 1;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->error_string &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "proc-receive failed to report status";
+		    code = -1;
+		}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int hook_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					hook_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (hook_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1413,7 +1679,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1453,7 +1719,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1489,6 +1755,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1518,6 +1785,21 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1544,6 +1826,14 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->error_string &&
+			    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED) &&
+			    (cmd->run_proc_receive || use_atomic))
+				cmd->error_string = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/remote.h b/remote.h
index 11d8719b58..ca7735b441 100644
--- a/remote.h
+++ b/remote.h
@@ -93,6 +93,14 @@ int for_each_remote(each_remote_fn fn, void *priv);
 
 int remote_has_url(struct remote *remote, const char *url);
 
+struct ref_push_report {
+	const char *ref_name;
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	unsigned int forced_update:1;
+	struct ref_push_report *next;
+};
+
 struct ref {
 	struct ref *next;
 	struct object_id old_oid;
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..42164d9898
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,176 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int die_version;
+static int die_readline;
+static int no_push_options;
+static int use_atomic;
+static int use_push_options;
+static int verbose;
+static int version = 1;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1 || die_version)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ' ||
+		    die_readline)
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	int nongit_ok = 0;
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_BOOL(0, "die-version", &die_version,
+			 "die during version negotiation"),
+		OPT_BOOL(0, "die-readline", &die_readline,
+			 "die when readline"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	setup_git_directory_gently(&nongit_ok);
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 590b2efca7..a0d3966b29 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -46,6 +46,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ddc8e990e9..07034d3f38 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -35,6 +35,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
new file mode 100644
index 0000000000..bb6ec92a92
--- /dev/null
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -0,0 +1,64 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
new file mode 100644
index 0000000000..4814f74dc2
--- /dev/null
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -0,0 +1,66 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
new file mode 100644
index 0000000000..c5fe4cb37b
--- /dev/null
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -0,0 +1,217 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: bad protocol version: 1
+	remote: error: proc-receive version "0" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
new file mode 100644
index 0000000000..53b47b0185
--- /dev/null
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -0,0 +1,160 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
new file mode 100644
index 0000000000..f726b7ca9c
--- /dev/null
+++ b/t/t5411/test-0020-report-ng.sh
@@ -0,0 +1,67 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
new file mode 100644
index 0000000000..fbf5569103
--- /dev/null
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -0,0 +1,69 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
new file mode 100644
index 0000000000..92a415b929
--- /dev/null
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
new file mode 100644
index 0000000000..acbf93e40a
--- /dev/null
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -0,0 +1,46 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
new file mode 100644
index 0000000000..c3946f329a
--- /dev/null
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
new file mode 100644
index 0000000000..d093b1a579
--- /dev/null
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
new file mode 100644
index 0000000000..d0c4da8b23
--- /dev/null
+++ b/t/t5411/test-0026-push-options.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
new file mode 100644
index 0000000000..c89a1e7c57
--- /dev/null
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -0,0 +1,82 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push \
+		--porcelain \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
new file mode 100644
index 0000000000..44c99d3831
--- /dev/null
+++ b/t/t5411/test-0030-report-ok.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
new file mode 100644
index 0000000000..3223b26184
--- /dev/null
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -0,0 +1,36 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
new file mode 100644
index 0000000000..a743aa8018
--- /dev/null
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -0,0 +1,256 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
new file mode 100644
index 0000000000..439b97b06e
--- /dev/null
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -0,0 +1,265 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/for/a/b/c/topic    [new reference]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
new file mode 100644
index 0000000000..aca2b0676c
--- /dev/null
+++ b/t/t5411/test-0034-report-ft.sh
@@ -0,0 +1,44 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
new file mode 100644
index 0000000000..30ffffb352
--- /dev/null
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (fall-through, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    <COMMIT-B>:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
new file mode 100644
index 0000000000..12acf9ea1f
--- /dev/null
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -0,0 +1,159 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
new file mode 100644
index 0000000000..f4a2c56e14
--- /dev/null
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -0,0 +1,162 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
new file mode 100644
index 0000000000..1dd6a97a97
--- /dev/null
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -0,0 +1,89 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic2
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
+	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
new file mode 100644
index 0000000000..32ebf63dcf
--- /dev/null
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -0,0 +1,91 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/bar    [new branch]
+	*    HEAD:refs/heads/baz    [new branch]
+	*    HEAD:refs/for/next/topic2    [new reference]
+	*    HEAD:refs/heads/foo    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
+	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 04/10] receive-pack: feed report options to post-receive
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (3 preceding siblings ...)
  2020-08-24 17:41                                           ` [PATCH v18 03/10] receive-pack: add new " Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 05/10] New capability "report-status-v2" for git-push Jiang Xin
                                                             ` (5 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When commands are fed to the "post-receive" hook, report options will
be parsed and the real old-oid, new-oid, reference name will feed to
the "post-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        | 30 ++++++++++++++++---
 t/t5411/test-0032-report-with-options.sh      | 14 ++++-----
 ...est-0033-report-with-options--porcelain.sh | 14 ++++-----
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 11 +++++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 11 +++++--
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 7 files changed, 58 insertions(+), 26 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2b63b92545..38f230248d 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -694,6 +694,7 @@ static void prepare_push_cert_sha1(struct child_process *proc)
 
 struct receive_hook_feed_state {
 	struct command *cmd;
+	struct ref_push_report *report;
 	int skip_broken;
 	struct strbuf buf;
 	const struct string_list *push_options;
@@ -781,11 +782,31 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
+	if (!bufp)
+		return 0; /* OK, can feed something. */
 	strbuf_reset(&state->buf);
-	strbuf_addf(&state->buf, "%s %s %s\n",
-		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
-		    cmd->ref_name);
-	state->cmd = cmd->next;
+	if (!state->report)
+		state->report = cmd->report;
+	if (state->report) {
+		struct object_id *old_oid;
+		struct object_id *new_oid;
+		const char *ref_name;
+
+		old_oid = state->report->old_oid ? state->report->old_oid : &cmd->old_oid;
+		new_oid = state->report->new_oid ? state->report->new_oid : &cmd->new_oid;
+		ref_name = state->report->ref_name ? state->report->ref_name : cmd->ref_name;
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(old_oid), oid_to_hex(new_oid),
+			    ref_name);
+		state->report = state->report->next;
+		if (!state->report)
+			state->cmd = cmd->next;
+	} else {
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
+			    cmd->ref_name);
+		state->cmd = cmd->next;
+	}
 	if (bufp) {
 		*bufp = state->buf.buf;
 		*sizep = state->buf.len;
@@ -804,6 +825,7 @@ static int run_receive_hook(struct command *commands,
 	strbuf_init(&state.buf, 0);
 	state.cmd = commands;
 	state.skip_broken = skip_broken;
+	state.report = NULL;
 	if (feed_receive_hook(&state, NULL, NULL))
 		return 0;
 	state.cmd = commands;
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a743aa8018..c559c207fa 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -54,7 +54,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -87,7 +87,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -121,7 +121,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -153,7 +153,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -187,7 +187,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -237,9 +237,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/next/topic
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 439b97b06e..ea9312234e 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -55,7 +55,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -90,7 +90,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -125,7 +125,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -158,7 +158,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -193,7 +193,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -245,9 +245,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/next/topic    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 12acf9ea1f..67cc4a8a4c 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -99,7 +101,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -145,7 +149,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index f4a2c56e14..1dc8551bc6 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -100,7 +102,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -147,7 +151,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 1dd6a97a97..bfc8d586d6 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 32ebf63dcf..5d021a4837 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
 	*    HEAD:refs/heads/bar    [new branch]
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 05/10] New capability "report-status-v2" for git-push
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (4 preceding siblings ...)
  2020-08-24 17:41                                           ` [PATCH v18 04/10] receive-pack: feed report options to post-receive Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 06/10] doc: add document for capability report-status-v2 Jiang Xin
                                                             ` (4 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin, Jeff King

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The new introduced "proc-receive" hook may handle a command for a
pseudo-reference with a zero-old as its old-oid, while the hook may
create or update a reference with different name, different new-oid,
and different old-oid (the reference may exist already with a non-zero
old-oid).  Current "report-status" protocol cannot report the status for
such reference rewrite.

Add new capability "report-status-v2" and new report protocol which is
not backward compatible for report of git-push.

If a user pushes to a pseudo-reference "refs/for/master/topic", and
"receive-pack" creates two new references "refs/changes/23/123/1" and
"refs/changes/24/124/1", for client without the knowledge of
"report-status-v2", "receive-pack" will only send "ok/ng" directives in
the report, such as:

    ok ref/for/master/topic

But for client which has the knowledge of "report-status-v2",
"receive-pack" will use "option" directives to report more attributes
for the reference given by the above "ok/ng" directive.

    ok refs/for/master/topic
    option refname refs/changes/23/123/1
    option new-oid <new-oid>
    ok refs/for/master/topic
    option refname refs/changes/24/124/1
    option new-oid <new-oid>

The client will report two new created references to the end user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        |  54 +++++++-
 builtin/send-pack.c                           |  19 +++
 remote.h                                      |   1 +
 send-pack.c                                   | 101 +++++++++++---
 t/t5411-proc-receive-hook.sh                  |   9 ++
 t/t5411/once-0010-report-status-v1.sh         |  90 ++++++++++++
 t/t5411/test-0032-report-with-options.sh      |  14 +-
 ...est-0033-report-with-options--porcelain.sh |  14 +-
 ...t-0036-report-multi-rewrite-for-one-ref.sh |  11 +-
 ...rt-multi-rewrite-for-one-ref--porcelain.sh |  11 +-
 t/t5411/test-0038-report-mixed-refs.sh        |   2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |   2 +-
 transport-helper.c                            | 128 ++++++++++++++----
 transport.c                                   | 116 ++++++++++++----
 14 files changed, 472 insertions(+), 100 deletions(-)
 create mode 100644 t/t5411/once-0010-report-status-v1.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 38f230248d..d6a216b2e3 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -56,6 +56,7 @@ static int advertise_push_options;
 static int unpack_limit = 100;
 static off_t max_input_size;
 static int report_status;
+static int report_status_v2;
 static int use_sideband;
 static int use_atomic;
 static int use_push_options;
@@ -239,7 +240,7 @@ static void show_ref(const char *path, const struct object_id *oid)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status report-status-v2 delete-refs side-band-64k quiet");
 		if (advertise_atomic_push)
 			strbuf_addstr(&cap, " atomic");
 		if (prefer_ofs_delta)
@@ -1937,6 +1938,8 @@ static struct command *read_head_info(struct packet_reader *reader,
 			const char *feature_list = reader->line + linelen + 1;
 			if (parse_feature_request(feature_list, "report-status"))
 				report_status = 1;
+			if (parse_feature_request(feature_list, "report-status-v2"))
+				report_status_v2 = 1;
 			if (parse_feature_request(feature_list, "side-band-64k"))
 				use_sideband = LARGE_PACKET_MAX;
 			if (parse_feature_request(feature_list, "quiet"))
@@ -2248,6 +2251,51 @@ static void report(struct command *commands, const char *unpack_status)
 	strbuf_release(&buf);
 }
 
+static void report_v2(struct command *commands, const char *unpack_status)
+{
+	struct command *cmd;
+	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report *report;
+
+	packet_buf_write(&buf, "unpack %s\n",
+			 unpack_status ? unpack_status : "ok");
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		int count = 0;
+
+		if (cmd->error_string) {
+			packet_buf_write(&buf, "ng %s %s\n",
+					 cmd->ref_name,
+					 cmd->error_string);
+			continue;
+		}
+		packet_buf_write(&buf, "ok %s\n",
+				 cmd->ref_name);
+		for (report = cmd->report; report; report = report->next) {
+			if (count++ > 0)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			if (report->ref_name)
+				packet_buf_write(&buf, "option refname %s\n",
+						 report->ref_name);
+			if (report->old_oid)
+				packet_buf_write(&buf, "option old-oid %s\n",
+						 oid_to_hex(report->old_oid));
+			if (report->new_oid)
+				packet_buf_write(&buf, "option new-oid %s\n",
+						 oid_to_hex(report->new_oid));
+			if (report->forced_update)
+				packet_buf_write(&buf, "option forced-update\n");
+		}
+	}
+	packet_buf_flush(&buf);
+
+	if (use_sideband)
+		send_sideband(1, 1, buf.buf, buf.len, use_sideband);
+	else
+		write_or_die(1, buf.buf, buf.len);
+	strbuf_release(&buf);
+}
+
 static int delete_only(struct command *commands)
 {
 	struct command *cmd;
@@ -2356,7 +2404,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
-		if (report_status)
+		if (report_status_v2)
+			report_v2(commands, unpack_status);
+		else if (report_status)
 			report(commands, unpack_status);
 		run_receive_hook(commands, "post-receive", 1,
 				 &push_options);
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index f2c5a34402..2777cc4371 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -29,10 +29,12 @@ static struct send_pack_args args;
 static void print_helper_status(struct ref *ref)
 {
 	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report *report;
 
 	for (; ref; ref = ref->next) {
 		const char *msg = NULL;
 		const char *res;
+		int count = 0;
 
 		switch(ref->status) {
 		case REF_STATUS_NONE:
@@ -94,6 +96,23 @@ static void print_helper_status(struct ref *ref)
 		}
 		strbuf_addch(&buf, '\n');
 
+		if (ref->status == REF_STATUS_OK) {
+			for (report = ref->report; report; report = report->next) {
+				if (count++ > 0)
+					strbuf_addf(&buf, "ok %s\n", ref->name);
+				if (report->ref_name)
+					strbuf_addf(&buf, "option refname %s\n",
+						report->ref_name);
+				if (report->old_oid)
+					strbuf_addf(&buf, "option old-oid %s\n",
+						oid_to_hex(report->old_oid));
+				if (report->new_oid)
+					strbuf_addf(&buf, "option new-oid %s\n",
+						oid_to_hex(report->new_oid));
+				if (report->forced_update)
+					strbuf_addstr(&buf, "option forced-update\n");
+			}
+		}
 		write_or_die(1, buf.buf, buf.len);
 	}
 	strbuf_release(&buf);
diff --git a/remote.h b/remote.h
index ca7735b441..6d1e5c49f8 100644
--- a/remote.h
+++ b/remote.h
@@ -148,6 +148,7 @@ struct ref {
 		REF_STATUS_ATOMIC_PUSH_FAILED
 	} status;
 	char *remote_status;
+	struct ref_push_report *report;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
 };
diff --git a/send-pack.c b/send-pack.c
index d1b7edc995..dfaf505c96 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -153,25 +153,79 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 {
 	struct ref *hint;
 	int ret;
+	struct ref_push_report *report = NULL;
+	int new_report = 0;
+	int once = 0;
 
 	hint = NULL;
 	ret = receive_unpack_status(reader);
 	while (1) {
+		struct object_id old_oid, new_oid;
+		const char *head;
 		const char *refname;
-		char *msg;
+		char *p;
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
-		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
-			error("invalid ref status from remote: %s", reader->line);
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			error("invalid status line from remote: %s", reader->line);
 			ret = -1;
 			break;
 		}
+		*p++ = '\0';
 
-		refname = reader->line + 3;
-		msg = strchr(refname, ' ');
-		if (msg)
-			*msg++ = '\0';
+		if (!strcmp(head, "option")) {
+			const char *key, *val;
 
+			if (!hint || !(report || new_report)) {
+				if (!once++)
+					error("'option' without a matching 'ok/ng' directive");
+				ret = -1;
+				continue;
+			}
+			if (new_report) {
+				if (!hint->report) {
+					hint->report = xcalloc(1, sizeof(struct ref_push_report));
+					report = hint->report;
+				} else {
+					report = hint->report;
+					while (report->next)
+						report = report->next;
+					report->next = xcalloc(1, sizeof(struct ref_push_report));
+					report = report->next;
+				}
+				new_report = 0;
+			}
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				report->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				report->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				report->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				report->forced_update = 1;
+			continue;
+		}
+
+		report = NULL;
+		new_report = 0;
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			error("invalid ref status from remote: %s", head);
+			ret = -1;
+			break;
+		}
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
 			hint = find_ref_by_name(hint, refname);
@@ -179,22 +233,27 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			hint = find_ref_by_name(refs, refname);
 		if (!hint) {
 			warning("remote reported status on unknown ref: %s",
-					refname);
+				refname);
 			continue;
 		}
-		if (hint->status != REF_STATUS_EXPECTING_REPORT) {
+		if (hint->status != REF_STATUS_EXPECTING_REPORT &&
+		    hint->status != REF_STATUS_OK &&
+		    hint->status != REF_STATUS_REMOTE_REJECT) {
 			warning("remote reported status on unexpected ref: %s",
-					refname);
+				refname);
 			continue;
 		}
-
-		if (reader->line[0] == 'o' && reader->line[1] == 'k')
-			hint->status = REF_STATUS_OK;
-		else
+		if (!strcmp(head, "ng")) {
 			hint->status = REF_STATUS_REMOTE_REJECT;
-		hint->remote_status = xstrdup_or_null(msg);
-		/* start our next search from the next ref */
-		hint = hint->next;
+			if (p)
+				hint->remote_status = xstrdup(p);
+			else
+				hint->remote_status = "failed";
+		} else {
+			hint->status = REF_STATUS_OK;
+			hint->remote_status = xstrdup_or_null(p);
+			new_report = 1;
+		}
 	}
 	return ret;
 }
@@ -369,7 +428,9 @@ int send_pack(struct send_pack_args *args,
 	struct packet_reader reader;
 
 	/* Does the other end support the reporting? */
-	if (server_supports("report-status"))
+	if (server_supports("report-status-v2"))
+		status_report = 2;
+	else if (server_supports("report-status"))
 		status_report = 1;
 	if (server_supports("delete-refs"))
 		allow_deleting_refs = 1;
@@ -418,8 +479,10 @@ int send_pack(struct send_pack_args *args,
 
 	use_push_options = push_options_supported && args->push_options;
 
-	if (status_report)
+	if (status_report == 1)
 		strbuf_addstr(&cap_buf, " report-status");
+	else if (status_report == 2)
+		strbuf_addstr(&cap_buf, " report-status-v2");
 	if (use_sideband)
 		strbuf_addstr(&cap_buf, " side-band-64k");
 	if (quiet_supported && (args->quiet || !args->progress))
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 3a684353a8..746487286f 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -78,6 +78,15 @@ run_proc_receive_hook_test() {
 # Initialize the upstream repository and local workbench.
 setup_upstream_and_workbench
 
+# Load test cases that only need to be executed once.
+for t in  "$TEST_DIRECTORY"/t5411/once-*.sh
+do
+	. "$t"
+done
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
 # Run test cases for 'proc-receive' hook on local file protocol.
 run_proc_receive_hook_test local
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
new file mode 100644
index 0000000000..bf410dc418
--- /dev/null
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -0,0 +1,90 @@
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic1" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/23/123/1" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/24/124/2" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update" \
+		-r "ng refs/for/next/topic target branch not exist"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic1(A)  foo(A)  refs/for/next/topic(A)  refs/for/master/topic2(A)
+test_expect_success "proc-receive: report status v1" '
+	{
+		if test -z "$GIT_DEFAULT_HASH" || test "$GIT_DEFAULT_HASH" = "sha1"
+		then
+			printf "%s %s refs/heads/master\0report-status\n" \
+				$A $B | packetize
+		else
+			printf "%s %s refs/heads/master\0report-status object-format=$GIT_DEFAULT_HASH\n" \
+				$A $B | packetize
+		fi &&
+		printf "%s %s refs/for/master/topic1\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/heads/foo\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/next/topic\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/master/topic2\n" \
+			$ZERO_OID $A | packetize &&
+		printf 0000 &&
+		printf "" | git -C "$upstream" pack-objects --stdout
+	} | git receive-pack "$upstream" --stateless-rpc \
+	>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	# pre-receive hook
+	pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	# proc-receive hook
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	proc-receive> ok refs/for/master/topic1
+	proc-receive> option fall-through
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/23/123/1
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/24/124/2
+	proc-receive> option old-oid <COMMIT-B>
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> option forced-update
+	proc-receive> ng refs/for/next/topic target branch not exist
+	000eunpack ok
+	0019ok refs/heads/master
+	001eok refs/for/master/topic1
+	0016ok refs/heads/foo
+	0033ng refs/for/next/topic target branch not exist
+	001eok refs/for/master/topic2
+	0000# post-receive hook
+	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/changes/23/123/1
+	post-receive< <COMMIT-B> <COMMIT-A> refs/for/changes/24/124/2
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/for/master/topic1
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index c559c207fa..b77b78c49f 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -56,7 +56,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -89,7 +89,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -123,7 +123,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -155,7 +155,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -189,7 +189,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -241,9 +241,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index ea9312234e..1fe352b686 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -57,7 +57,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -92,7 +92,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -127,7 +127,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -160,7 +160,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -195,7 +195,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual
@@ -249,9 +249,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
-	*    HEAD:refs/for/master/topic    [new reference]
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 67cc4a8a4c..27d58edd14 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -45,7 +45,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -105,7 +107,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -152,7 +156,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index 1dc8551bc6..77b5b22ed4 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -45,7 +45,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -106,7 +108,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -154,7 +158,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/23/123/1    [new reference]
+	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index bfc8d586d6..a74a2cb449 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic2
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 5d021a4837..e4baa13ea3 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	*    HEAD:refs/heads/baz    [new branch]
 	*    HEAD:refs/for/next/topic2    [new reference]
 	*    HEAD:refs/heads/foo    [new branch]
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
 	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
 	Done
diff --git a/transport-helper.c b/transport-helper.c
index a46afcb69d..1738c81ea0 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -719,13 +719,61 @@ static int fetch(struct transport *transport,
 	return -1;
 }
 
+struct push_update_ref_state {
+	struct ref *hint;
+	struct ref_push_report *report;
+	int new_report;
+};
+
 static int push_update_ref_status(struct strbuf *buf,
-				   struct ref **ref,
+				   struct push_update_ref_state *state,
 				   struct ref *remote_refs)
 {
 	char *refname, *msg;
 	int status, forced = 0;
 
+	if (starts_with(buf->buf, "option ")) {
+		struct object_id old_oid, new_oid;
+		const char *key, *val;
+		char *p;
+
+		if (!state->hint || !(state->report || state->new_report))
+			die(_("'option' without a matching 'ok/error' directive"));
+		if (state->new_report) {
+			if (!state->hint->report) {
+				state->hint->report = xcalloc(1, sizeof(struct ref_push_report));
+				state->report = state->hint->report;
+			} else {
+				state->report = state->hint->report;
+				while (state->report->next)
+					state->report = state->report->next;
+				state->report->next = xcalloc(1, sizeof(struct ref_push_report));
+				state->report = state->report->next;
+			}
+			state->new_report = 0;
+		}
+		key = buf->buf + 7;
+		p = strchr(key, ' ');
+		if (p)
+			*p++ = '\0';
+		val = p;
+		if (!strcmp(key, "refname"))
+			state->report->ref_name = xstrdup_or_null(val);
+		else if (!strcmp(key, "old-oid") && val &&
+			 !parse_oid_hex(val, &old_oid, &val))
+			state->report->old_oid = oiddup(&old_oid);
+		else if (!strcmp(key, "new-oid") && val &&
+			 !parse_oid_hex(val, &new_oid, &val))
+			state->report->new_oid = oiddup(&new_oid);
+		else if (!strcmp(key, "forced-update"))
+			state->report->forced_update = 1;
+		/* Not update remote namespace again. */
+		return 1;
+	}
+
+	state->report = NULL;
+	state->new_report = 0;
+
 	if (starts_with(buf->buf, "ok ")) {
 		status = REF_STATUS_OK;
 		refname = buf->buf + 3;
@@ -781,16 +829,16 @@ static int push_update_ref_status(struct strbuf *buf,
 		}
 	}
 
-	if (*ref)
-		*ref = find_ref_by_name(*ref, refname);
-	if (!*ref)
-		*ref = find_ref_by_name(remote_refs, refname);
-	if (!*ref) {
+	if (state->hint)
+		state->hint = find_ref_by_name(state->hint, refname);
+	if (!state->hint)
+		state->hint = find_ref_by_name(remote_refs, refname);
+	if (!state->hint) {
 		warning(_("helper reported unexpected status of %s"), refname);
 		return 1;
 	}
 
-	if ((*ref)->status != REF_STATUS_NONE) {
+	if (state->hint->status != REF_STATUS_NONE) {
 		/*
 		 * Earlier, the ref was marked not to be pushed, so ignore the ref
 		 * status reported by the remote helper if the latter is 'no match'.
@@ -799,9 +847,11 @@ static int push_update_ref_status(struct strbuf *buf,
 			return 1;
 	}
 
-	(*ref)->status = status;
-	(*ref)->forced_update |= forced;
-	(*ref)->remote_status = msg;
+	if (status == REF_STATUS_OK)
+		state->new_report = 1;
+	state->hint->status = status;
+	state->hint->forced_update |= forced;
+	state->hint->remote_status = msg;
 	return !(status == REF_STATUS_OK);
 }
 
@@ -809,37 +859,57 @@ static int push_update_refs_status(struct helper_data *data,
 				    struct ref *remote_refs,
 				    int flags)
 {
+	struct ref *ref;
+	struct ref_push_report *report;
 	struct strbuf buf = STRBUF_INIT;
-	struct ref *ref = remote_refs;
-	int ret = 0;
+	struct push_update_ref_state state = { remote_refs, NULL, 0 };
 
 	for (;;) {
-		char *private;
-
 		if (recvline(data, &buf)) {
-			ret = 1;
-			break;
+			strbuf_release(&buf);
+			return 1;
 		}
-
 		if (!buf.len)
 			break;
+		push_update_ref_status(&buf, &state, remote_refs);
+	}
+	strbuf_release(&buf);
 
-		if (push_update_ref_status(&buf, &ref, remote_refs))
-			continue;
+	if (flags & TRANSPORT_PUSH_DRY_RUN || !data->rs.nr || data->no_private_update)
+		return 0;
 
-		if (flags & TRANSPORT_PUSH_DRY_RUN || !data->rs.nr || data->no_private_update)
-			continue;
+	/* propagate back the update to the remote namespace */
+	for (ref = remote_refs; ref; ref = ref->next) {
+		char *private;
 
-		/* propagate back the update to the remote namespace */
-		private = apply_refspecs(&data->rs, ref->name);
-		if (!private)
+		if (ref->status != REF_STATUS_OK)
 			continue;
-		update_ref("update by helper", private, &ref->new_oid, NULL,
-			   0, 0);
-		free(private);
+
+		if (!ref->report) {
+			private = apply_refspecs(&data->rs, ref->name);
+			if (!private)
+				continue;
+			update_ref("update by helper", private, &(ref->new_oid),
+				   NULL, 0, 0);
+			free(private);
+		} else {
+			for (report = ref->report; report; report = report->next) {
+				private = apply_refspecs(&data->rs,
+							 report->ref_name
+							 ? report->ref_name
+							 : ref->name);
+				if (!private)
+					continue;
+				update_ref("update by helper", private,
+					   report->new_oid
+					   ? report->new_oid
+					   : &(ref->new_oid),
+					   NULL, 0, 0);
+				free(private);
+			}
+		}
 	}
-	strbuf_release(&buf);
-	return ret;
+	return 0;
 }
 
 static void set_common_push_options(struct transport *transport,
diff --git a/transport.c b/transport.c
index 26fefd13c1..e146de6c4e 100644
--- a/transport.c
+++ b/transport.c
@@ -461,13 +461,21 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
+			     struct ref_push_report *report,
 			     int porcelain, int summary_width)
 {
+	const char *to_name;
+
+	if (report && report->ref_name)
+		to_name = report->ref_name;
+	else
+		to_name = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +489,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -493,27 +503,52 @@ static void print_ref_status(char flag, const char *summary,
 	}
 }
 
-static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
+static void print_ok_ref_status(struct ref *ref,
+				struct ref_push_report *report,
+				int porcelain, int summary_width)
 {
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	const char *ref_name;
+	int forced_update;
+
+	if (report && report->old_oid)
+		old_oid = report->old_oid;
+	else
+		old_oid = &ref->old_oid;
+	if (report && report->new_oid)
+		new_oid = report->new_oid;
+	else
+		new_oid = &ref->new_oid;
+	if (report && report->forced_update)
+		forced_update = report->forced_update;
+	else
+		forced_update = ref->forced_update;
+	if (report && report->ref_name)
+		ref_name = report->ref_name;
+	else
+		ref_name = ref->name;
+
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
-				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+				 report, porcelain, summary_width);
+	else if (is_null_oid(old_oid))
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(ref_name, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(ref_name, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 ref, ref->peer_ref, NULL,
+				 report, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
 
-		strbuf_add_unique_abbrev(&quickref, &ref->old_oid,
+		strbuf_add_unique_abbrev(&quickref, old_oid,
 					 DEFAULT_ABBREV);
-		if (ref->forced_update) {
+		if (forced_update) {
 			strbuf_addstr(&quickref, "...");
 			type = '+';
 			msg = "forced update";
@@ -522,16 +557,17 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 			type = ' ';
 			msg = NULL;
 		}
-		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
+		strbuf_add_unique_abbrev(&quickref, new_oid,
 					 DEFAULT_ABBREV);
 
 		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
+static int print_one_push_report(struct ref *ref, const char *dest, int count,
+				 struct ref_push_report *report,
 				 int porcelain, int summary_width)
 {
 	if (!count) {
@@ -543,65 +579,89 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	switch(ref->status) {
 	case REF_STATUS_NONE:
 		print_ref_status('X', "[no match]", ref, NULL, NULL,
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
 		print_ref_status('!', "[rejected]", ref, NULL,
 				 "remote does not support deleting refs",
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
 		print_ref_status('=', "[up to date]", ref,
-				 ref->peer_ref, NULL, porcelain, summary_width);
+				 ref->peer_ref, NULL,
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "non-fast-forward", porcelain, summary_width);
+				 "non-fast-forward",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "already exists", porcelain, summary_width);
+				 "already exists",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "fetch first", porcelain, summary_width);
+				 "fetch first",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "needs force", porcelain, summary_width);
+				 "needs force",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "stale info", porcelain, summary_width);
+				 "stale info",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
 				 "new shallow roots not allowed",
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
 		print_ref_status('!', "[remote rejected]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
-				 ref->remote_status, porcelain, summary_width);
+				 ref->remote_status,
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
 		print_ref_status('!', "[remote failure]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "atomic push failed", porcelain, summary_width);
+				 "atomic push failed",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-		print_ok_ref_status(ref, porcelain, summary_width);
+		print_ok_ref_status(ref, report, porcelain, summary_width);
 		break;
 	}
 
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	struct ref_push_report *report;
+	int n = 0;
+
+	if (!ref->report)
+		return print_one_push_report(ref, dest, count,
+					     NULL, porcelain, summary_width);
+
+	for (report = ref->report; report; report = report->next)
+		print_one_push_report(ref, dest, count + n++,
+				      report, porcelain, summary_width);
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 06/10] doc: add document for capability report-status-v2
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (5 preceding siblings ...)
  2020-08-24 17:41                                           ` [PATCH v18 05/10] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-24 17:41                                           ` [PATCH v18 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
                                                             ` (3 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add ABNF notation for capability 'report-status-v2' which extends
capability 'report-status' by adding additional option lines.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/technical/pack-protocol.txt     | 43 +++++++++++++++++--
 .../technical/protocol-capabilities.txt       | 17 ++++++--
 2 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt
index d5ce4eea8a..7547b9ca89 100644
--- a/Documentation/technical/pack-protocol.txt
+++ b/Documentation/technical/pack-protocol.txt
@@ -503,8 +503,8 @@ The reference discovery phase is done nearly the same way as it is in the
 fetching protocol. Each reference obj-id and name on the server is sent
 in packet-line format to the client, followed by a flush-pkt.  The only
 real difference is that the capability listing is different - the only
-possible values are 'report-status', 'delete-refs', 'ofs-delta' and
-'push-options'.
+possible values are 'report-status', 'report-status-v2', 'delete-refs',
+'ofs-delta', 'atomic' and 'push-options'.
 
 Reference Update Request and Packfile Transfer
 ----------------------------------------------
@@ -625,7 +625,7 @@ Report Status
 -------------
 
 After receiving the pack data from the sender, the receiver sends a
-report if 'report-status' capability is in effect.
+report if 'report-status' or 'report-status-v2' capability is in effect.
 It is a short listing of what happened in that update.  It will first
 list the status of the packfile unpacking as either 'unpack ok' or
 'unpack [error]'.  Then it will list the status for each of the references
@@ -647,6 +647,43 @@ update was successful, or 'ng [refname] [error]' if the update was not.
   error-msg         = 1*(OCTET) ; where not "ok"
 ----
 
+New capability "report-status-v2" extends the protocol by adding new option
+lines in order to support reporting of reference rewritten by the
+"proc-receive" hook.  The "proc-receive" hook may handle a command for a
+pseudo-reference which may create or update one or more references, and each
+reference may have different name, different new-oid, and different old-oid.
+
+----
+  report-status-v2  = unpack-status
+		      1*(command-status-v2)
+		      flush-pkt
+
+  unpack-status     = PKT-LINE("unpack" SP unpack-result)
+  unpack-result     = "ok" / error-msg
+
+  command-status-v2 = command-ok-v2 / command-fail-v2
+  command-ok-v2     = command-ok
+		      *option-line
+  command-fail-v2   = command-fail
+		      *option-line
+
+  command-ok        = PKT-LINE("ok" SP refname)
+  command-fail      = PKT-LINE("ng" SP refname SP error-msg)
+
+  error-msg         = 1*(OCTET) ; where not "ok"
+
+  option-line       = *1(option-refname)
+		      *1(option-old-oid)
+		      *1(option-new-oid)
+		      *1(option-forced-update)
+
+  option-refname    = PKT-LINE("option" SP "refname" SP refname)
+  option-old-oid    = PKT-LINE("option" SP "old-oid" SP obj-id)
+  option-new-oid    = PKT-LINE("option" SP "new-oid" SP obj-id)
+  option-force      = PKT-LINE("option" SP "forced-update")
+
+----
+
 Updates can be unsuccessful for a number of reasons.  The reference can have
 changed since the reference discovery phase was originally sent, meaning
 someone pushed in the meantime.  The reference being pushed could be a
diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 2b267c0da6..91fd976094 100644
--- a/Documentation/technical/protocol-capabilities.txt
+++ b/Documentation/technical/protocol-capabilities.txt
@@ -22,9 +22,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 'atomic', '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', 'report-status-v2', '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
@@ -269,6 +269,17 @@ each reference was updated successfully.  If any of those were not
 successful, it will send back an error message.  See pack-protocol.txt
 for example messages.
 
+report-status-v2
+----------------
+
+Capability 'report-status-v2' extends capability 'report-status' by
+adding new option lines in order to support reference rewritten by
+the"proc-receive" hook.  The "proc-receive" hook may handle a command
+for a pseudo-reference which may create or update a reference with
+different name, new-oid, and old-oid.  While the capability
+"report-status" cannot report for such case.  See pack-protocol.txt
+for details.
+
 delete-refs
 -----------
 
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 07/10] receive-pack: new config receive.procReceiveRefs
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (6 preceding siblings ...)
  2020-08-24 17:41                                           ` [PATCH v18 06/10] doc: add document for capability report-status-v2 Jiang Xin
@ 2020-08-24 17:41                                           ` Jiang Xin
  2020-08-24 17:42                                           ` [PATCH v18 08/10] t5411: test updates of remote-tracking branches Jiang Xin
                                                             ` (2 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:41 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings given by the config variables match the
reference names of the commands which are sent from git client to
`receive-pack`, these commands will be executed by an external hook
(named "proc-receive"), instead of the internal `execute_commands`
function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Optional modifiers can be provided in the beginning of the value to
filter commands for specific actions: create (a), mofify (m),
delete (d). A `!` can be included in the modifiers to negate the
reference prefix entry. E.g.:

    git config --system --add receive.procReceiveRefs ad:refs/heads

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt              |  21 +++
 builtin/receive-pack.c                        | 106 ++++++++++++++-
 t/t5411/once-0010-report-status-v1.sh         |   4 +
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0040-process-all-refs.sh         | 113 ++++++++++++++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 ++++++++++++++++
 t/t5411/test-0050-proc-receive-refs.sh        | 128 ++++++++++++++++++
 7 files changed, 486 insertions(+), 7 deletions(-)
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh
 create mode 100644 t/t5411/test-0050-proc-receive-refs.sh

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..86bb9adeb7 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,27 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
++
+For example, if this variable is set to "refs/for", pushing to reference
+such as "refs/for/master" will not create or update a reference named
+"refs/for/master", but may create or update a pull request directly by
+running the hook "proc-receive".
++
+Optional modifiers can be provided in the beginning of the value to filter
+commands for specific actions: create (a), mofify (m), delete (d).
+A `!` can be included in the modifiers to negate the reference prefix entry.
+E.g.:
++
+	git config --system --add receive.procReceiveRefs ad:refs/heads
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index d6a216b2e3..3eecb8c33c 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -97,6 +97,17 @@ static int keepalive_in_sec = 5;
 
 static struct tmp_objdir *tmp_objdir;
 
+static struct proc_receive_ref {
+	unsigned int want_add:1,
+		     want_delete:1,
+		     want_modify:1,
+		     negative_ref:1;
+	char *ref_prefix;
+	struct proc_receive_ref *next;
+} *proc_receive_ref;
+
+static void proc_receive_ref_append(const char *prefix);
+
 static enum deny_action parse_deny_action(const char *var, const char *value)
 {
 	if (value) {
@@ -229,6 +240,13 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		if (!value)
+			return config_error_nonbool(var);
+		proc_receive_ref_append(value);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -324,6 +342,79 @@ struct command {
 	char ref_name[FLEX_ARRAY]; /* more */
 };
 
+static void proc_receive_ref_append(const char *prefix)
+{
+	struct proc_receive_ref *ref_pattern;
+	char *p;
+	int len;
+
+	ref_pattern = xcalloc(1, sizeof(struct proc_receive_ref));
+	p = strchr(prefix, ':');
+	if (p) {
+		while (prefix < p) {
+			if (*prefix == 'a')
+				ref_pattern->want_add = 1;
+			else if (*prefix == 'd')
+				ref_pattern->want_delete = 1;
+			else if (*prefix == 'm')
+				ref_pattern->want_modify = 1;
+			else if (*prefix == '!')
+				ref_pattern->negative_ref = 1;
+			prefix++;
+		}
+		prefix++;
+	} else {
+		ref_pattern->want_add = 1;
+		ref_pattern->want_delete = 1;
+		ref_pattern->want_modify = 1;
+	}
+	len = strlen(prefix);
+	while (len && prefix[len - 1] == '/')
+		len--;
+	ref_pattern->ref_prefix = xmemdupz(prefix, len);
+	if (!proc_receive_ref) {
+		proc_receive_ref = ref_pattern;
+	} else {
+		struct proc_receive_ref *end;
+
+		end = proc_receive_ref;
+		while (end->next)
+			end = end->next;
+		end->next = ref_pattern;
+	}
+}
+
+static int proc_receive_ref_matches(struct command *cmd)
+{
+	struct proc_receive_ref *p;
+
+	if (!proc_receive_ref)
+		return 0;
+
+	for (p = proc_receive_ref; p; p = p->next) {
+		const char *match = p->ref_prefix;
+		const char *remains;
+
+		if (!p->want_add && is_null_oid(&cmd->old_oid))
+			continue;
+		else if (!p->want_delete && is_null_oid(&cmd->new_oid))
+			continue;
+		else if (!p->want_modify &&
+			 !is_null_oid(&cmd->old_oid) &&
+			 !is_null_oid(&cmd->new_oid))
+			continue;
+
+		if (skip_prefix(cmd->ref_name, match, &remains) &&
+		    (!*remains || *remains == '/')) {
+			if (!p->negative_ref)
+				return 1;
+		} else if (p->negative_ref) {
+			return 1;
+		}
+	}
+	return 0;
+}
+
 static void rp_error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 static void rp_warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 
@@ -1812,14 +1903,15 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_ref) {
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+			if (proc_receive_ref_matches(cmd)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
 	}
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index bf410dc418..dc2cf4a522 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -1,3 +1,7 @@
+test_expect_success "setup receive.procReceiveRefs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/for
+'
+
 test_expect_success "setup proc-receive hook" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
diff --git a/t/t5411/test-0010-proc-receive-settings.sh b/t/t5411/test-0010-proc-receive-settings.sh
new file mode 100644
index 0000000000..a36809927b
--- /dev/null
+++ b/t/t5411/test-0010-proc-receive-settings.sh
@@ -0,0 +1,7 @@
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
new file mode 100644
index 0000000000..b07c999f53
--- /dev/null
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -0,0 +1,113 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
new file mode 100644
index 0000000000..0dd9824616
--- /dev/null
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -0,0 +1,114 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+	-    :refs/heads/foo    [deleted]
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0050-proc-receive-refs.sh b/t/t5411/test-0050-proc-receive-refs.sh
new file mode 100644
index 0000000000..85e4784678
--- /dev/null
+++ b/t/t5411/test-0050-proc-receive-refs.sh
@@ -0,0 +1,128 @@
+test_expect_success "config receive.procReceiveRefs with modifiers ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git config --unset-all receive.procReceiveRefs &&
+		git config --add receive.procReceiveRefs m:refs/heads/master &&
+		git config --add receive.procReceiveRefs ad:refs/heads &&
+		git config --add receive.procReceiveRefs "a!:refs/heads"
+	)
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/tags/v123 " \
+		-r "option refname refs/pull/124/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : master(B)  tags/v123
+test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		v123 >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/tags/v123
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> refs/pull/123/head
+	 * [new reference] v123 -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $ZERO_OID" \
+		-r "ok refs/heads/next" \
+		-r "option refname refs/pull/124/head" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)  tags/v123
+# Refs of workbench: master(A)  tags/v123
+# git push         : NULL       NULL       next(A)
+test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOCOL)" '
+	git -C workbench push origin \
+		:refs/heads/master \
+		$A:refs/heads/next \
+		:refs/tags/v123 >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
+	remote: pre-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <ZERO-OID>
+	remote: proc-receive> ok refs/heads/next
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/pull/123/head
+	remote: post-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 - [deleted] refs/pull/123/head
+	 - [deleted] v123
+	 * [new reference] <COMMIT-A> -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 08/10] t5411: test updates of remote-tracking branches
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (7 preceding siblings ...)
  2020-08-24 17:41                                           ` [PATCH v18 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-08-24 17:42                                           ` Jiang Xin
  2020-08-24 17:42                                           ` [PATCH v18 09/10] transport: parse report options for tracking refs Jiang Xin
  2020-08-24 17:42                                           ` [PATCH v18 10/10] doc: add documentation for the proc-receive hook Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:42 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

In order to test update of remote-tracking branches for special refs,
add new "remote.origin.fetch" settings and test cases.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 48 +++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 27d58edd14..8eec3a1ed8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -1,3 +1,18 @@
+test_expect_success "setup git config for remote-tracking of special refs" '
+	(
+		cd workbench &&
+		if ! git config --get-all remote.origin.fetch | grep refs/for/
+		then
+			git config --add remote.origin.fetch \
+				"+refs/for/*:refs/t/for/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/pull/*:refs/t/pull/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/changes/*:refs/t/changes/*"
+		fi
+	)
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -58,6 +73,17 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -120,6 +146,17 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -167,3 +204,14 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	EOF
 	test_cmp expect actual
 '
+
+test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 09/10] transport: parse report options for tracking refs
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (8 preceding siblings ...)
  2020-08-24 17:42                                           ` [PATCH v18 08/10] t5411: test updates of remote-tracking branches Jiang Xin
@ 2020-08-24 17:42                                           ` Jiang Xin
  2020-08-24 17:42                                           ` [PATCH v18 10/10] doc: add documentation for the proc-receive hook Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:42 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a pseudo reference (such as "refs/for/master/topic"), may
create or update one or more references.  The real names of the
references will be stored in the report options.  Parse report options
to create or update remote-tracking branches properly.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 22 +++++++---
 transport.c                                   | 40 ++++++++++++++-----
 2 files changed, 46 insertions(+), 16 deletions(-)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 8eec3a1ed8..73283d81e8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -78,10 +78,14 @@ test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-B> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
@@ -151,10 +155,14 @@ test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-A> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
@@ -210,8 +218,10 @@ test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/23/123/1
+	<COMMIT-B> refs/t/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/2
 '
diff --git a/transport.c b/transport.c
index e146de6c4e..7d37baea64 100644
--- a/transport.c
+++ b/transport.c
@@ -437,28 +437,48 @@ int transport_refs_pushed(struct ref *ref)
 	return 0;
 }
 
-void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+static void update_tracking_ref(struct remote *remote, char *refname,
+				struct object_id *new_oid, int deletion,
+				int verbose)
 {
 	struct refspec_item rs;
 
-	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
-		return;
-
-	rs.src = ref->name;
+	rs.src = refname;
 	rs.dst = NULL;
-
 	if (!remote_find_tracking(remote, &rs)) {
 		if (verbose)
 			fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst);
-		if (ref->deletion) {
+		if (deletion)
 			delete_ref(NULL, rs.dst, NULL, 0);
-		} else
-			update_ref("update by push", rs.dst, &ref->new_oid,
-				   NULL, 0, 0);
+		else
+			update_ref("update by push", rs.dst, new_oid,
+				NULL, 0, 0);
 		free(rs.dst);
 	}
 }
 
+void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+{
+	char *refname;
+	struct object_id *new_oid;
+	struct ref_push_report *report;
+
+	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
+		return;
+
+	report = ref->report;
+	if (!report)
+		update_tracking_ref(remote, ref->name, &ref->new_oid,
+				    ref->deletion, verbose);
+	else
+		for (; report; report = report->next) {
+			refname = report->ref_name ? (char *)report->ref_name : ref->name;
+			new_oid = report->new_oid ? report->new_oid : &ref->new_oid;
+			update_tracking_ref(remote, refname, new_oid,
+					    is_null_oid(new_oid), verbose);
+		}
+}
+
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     struct ref_push_report *report,
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v18 10/10] doc: add documentation for the proc-receive hook
  2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
                                                             ` (9 preceding siblings ...)
  2020-08-24 17:42                                           ` [PATCH v18 09/10] transport: parse report options for tracking refs Jiang Xin
@ 2020-08-24 17:42                                           ` Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-24 17:42 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

"git receive-pack" that accepts requests by "git push" learned to
outsource some of the ref updates to the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 62 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 81f2a87e88..d66154f606 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,68 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option fall-through)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status can be given in options
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option refname <refname>)
+    H: PKT-LINE(option old-oid <old-oid>)
+    H: PKT-LINE(option new-oid <new-oid>)
+    H: PKT-LINE(option forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook will use
+"option" directives to report extended attributes for the reference given
+by the above "ok/ng" directive.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.26.2.543.ge7de2a6e37


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 00/10] New proc-receive hook for centralized workflow
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 19:57                                               ` Junio C Hamano
  2020-08-27 15:45                                             ` [PATCH v19 01/10] transport: not report a non-head push as a branch Jiang Xin
                                                               ` (9 subsequent siblings)
  10 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

## Changes since v18

1. This series is based on "next" branch, in order to resolve a conflict
   with commit 95e7c38539 (refspec: make sure stack refspec_item
   variables are zeroed, 2020-08-14).  See patch 9/10.

2. Fix typos in patch 3/10, 6/10, 7/10.

3. Fix ABNF syntax for capability "report-status-v2" in patch 6/10.

4. Add more test cases in patch 7/10 to increase code coverage.


## Range diff

 1:  22a9252587 =  1:  96c6d41872 transport: not report a non-head push as a branch
 2:  5c3fd7eb7a =  2:  861507a38e t5411: add basic test cases for proc-receive hook
 3:  003a36b71f !  3:  4b6d00a671 receive-pack: add new proc-receive hook
    @@ Commit message
         The filter will check the prefix of the reference name, and if the
         command has a special reference name, the filter will turn a specific
         field (`run_proc_receive`) on for the command.  Commands with this filed
    -    turned on will be executed by a new handler (an hook named
    +    turned on will be executed by a new handler (a hook named
         "proc-receive") instead of the internal `execute_commands` function.
         We can use this "proc-receive" command to create pull requests or send
         emails for code review.
     
         Suggested by Junio, this "proc-receive" hook reads the commands,
         push-options (optional), and send result using a protocol in pkt-line
    -    format.  In the following example, The letter "S" stands for
    +    format.  In the following example, the letter "S" stands for
         "receive-pack" and letter "H" stands for the hook.
     
             # Version and features negotiation.
 4:  60b9157a8e =  4:  b3986f98eb receive-pack: feed report options to post-receive
 5:  4a9b7187af !  5:  882f391b4f New capability "report-status-v2" for git-push
    @@ builtin/receive-pack.c: static void show_ref(const char *path, const struct obje
      			strbuf_addstr(&cap, " atomic");
      		if (prefer_ofs_delta)
     @@ builtin/receive-pack.c: static struct command *read_head_info(struct packet_reader *reader,
    - 			const char *feature_list = reader->line + linelen + 1;
    + 			int len = 0;
      			if (parse_feature_request(feature_list, "report-status"))
      				report_status = 1;
     +			if (parse_feature_request(feature_list, "report-status-v2"))
 6:  940acbc406 !  6:  331bfd4697 doc: add document for capability report-status-v2
    @@ Documentation/technical/pack-protocol.txt: update was successful, or 'ng [refnam
        error-msg         = 1*(OCTET) ; where not "ok"
      ----
      
    -+New capability "report-status-v2" extends the protocol by adding new option
    ++The 'report-status-v2' capability extends the protocol by adding new option
     +lines in order to support reporting of reference rewritten by the
    -+"proc-receive" hook.  The "proc-receive" hook may handle a command for a
    ++'proc-receive' hook.  The 'proc-receive' hook may handle a command for a
     +pseudo-reference which may create or update one or more references, and each
     +reference may have different name, different new-oid, and different old-oid.
     +
    @@ Documentation/technical/pack-protocol.txt: update was successful, or 'ng [refnam
     +  unpack-status     = PKT-LINE("unpack" SP unpack-result)
     +  unpack-result     = "ok" / error-msg
     +
    -+  command-status-v2 = command-ok-v2 / command-fail-v2
    ++  command-status-v2 = command-ok-v2 / command-fail
     +  command-ok-v2     = command-ok
     +		      *option-line
    -+  command-fail-v2   = command-fail
    -+		      *option-line
     +
     +  command-ok        = PKT-LINE("ok" SP refname)
     +  command-fail      = PKT-LINE("ng" SP refname SP error-msg)
    @@ Documentation/technical/protocol-capabilities.txt: each reference was updated su
     +----------------
     +
     +Capability 'report-status-v2' extends capability 'report-status' by
    -+adding new option lines in order to support reference rewritten by
    -+the"proc-receive" hook.  The "proc-receive" hook may handle a command
    ++adding new "option" directives in order to support reference rewritten by
    ++the "proc-receive" hook.  The "proc-receive" hook may handle a command
     +for a pseudo-reference which may create or update a reference with
     +different name, new-oid, and old-oid.  While the capability
    -+"report-status" cannot report for such case.  See pack-protocol.txt
    ++'report-status' cannot report for such case.  See pack-protocol.txt
     +for details.
     +
      delete-refs
 7:  974a06e15e !  7:  d5465fc22b receive-pack: new config receive.procReceiveRefs
    @@ Commit message
         "proc-receive".
     
         Optional modifiers can be provided in the beginning of the value to
    -    filter commands for specific actions: create (a), mofify (m),
    +    filter commands for specific actions: create (a), modify (m),
         delete (d). A `!` can be included in the modifiers to negate the
         reference prefix entry. E.g.:
     
             git config --system --add receive.procReceiveRefs ad:refs/heads
    +        git config --system --add receive.procReceiveRefs !:refs/heads
     
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
    @@ Documentation/config/receive.txt: receive.hideRefs::
     +running the hook "proc-receive".
     ++
     +Optional modifiers can be provided in the beginning of the value to filter
    -+commands for specific actions: create (a), mofify (m), delete (d).
    ++commands for specific actions: create (a), modify (m), delete (d).
     +A `!` can be included in the modifiers to negate the reference prefix entry.
     +E.g.:
     ++
     +	git config --system --add receive.procReceiveRefs ad:refs/heads
    ++	git config --system --add receive.procReceiveRefs !:refs/heads
     +
      receive.updateServerInfo::
      	If set to true, git-receive-pack will run git-update-server-info
    @@ t/t5411/test-0041-process-all-refs--porcelain.sh (new)
     +	)
     +'
     
    - ## t/t5411/test-0050-proc-receive-refs.sh (new) ##
    + ## t/t5411/test-0050-proc-receive-refs-with-modifiers.sh (new) ##
     @@
     +test_expect_success "config receive.procReceiveRefs with modifiers ($PROTOCOL)" '
     +	(
    @@ t/t5411/test-0050-proc-receive-refs.sh (new)
     +# Refs of upstream : master(A)
     +# Refs of workbench: master(A)  tags/v123
     +test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
    ++	git -C "$upstream" update-ref refs/heads/topic $A &&
     +	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    ++	<COMMIT-A> refs/heads/topic
     +	<TAG-v123> refs/tags/v123
     +	EOF
     +	test_cmp expect actual
    @@ t/t5411/test-0050-proc-receive-refs.sh (new)
     +	EOF
     +'
     +
    -+# Refs of upstream : master(A)  tags/v123
    -+# Refs of workbench: master(A)  tags/v123
    -+# git push         : NULL       NULL       next(A)
    ++# Refs of upstream : master(A)  topic(A)  tags/v123
    ++# Refs of workbench: master(A)            tags/v123
    ++# git push         : NULL       topic(B)  NULL       next(A)
     +test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOCOL)" '
     +	git -C workbench push origin \
     +		:refs/heads/master \
    ++		$B:refs/heads/topic \
     +		$A:refs/heads/next \
     +		:refs/tags/v123 >out 2>&1 &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	remote: # pre-receive hook
     +	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
    ++	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/topic
     +	remote: pre-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
     +	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
     +	remote: # proc-receive hook
    @@ t/t5411/test-0050-proc-receive-refs.sh (new)
     +	remote: proc-receive> option new-oid <COMMIT-A>
     +	remote: # post-receive hook
     +	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/pull/123/head
    ++	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/topic
     +	remote: post-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
     +	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/124/head
     +	To <URL/of/upstream.git>
     +	 - [deleted] refs/pull/123/head
    ++	 <OID-A>..<OID-B> <COMMIT-B> -> topic
     +	 - [deleted] v123
     +	 * [new reference] <COMMIT-A> -> refs/pull/124/head
     +	EOF
    @@ t/t5411/test-0050-proc-receive-refs.sh (new)
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
     +	<COMMIT-A> refs/heads/master
    ++	<COMMIT-B> refs/heads/topic
     +	EOF
     +	test_cmp expect actual
     +'
 8:  5b588f7926 =  8:  cd38e2ddf5 t5411: test updates of remote-tracking branches
 9:  7c707984e3 !  9:  342784222f transport: parse report options for tracking refs
    @@ transport.c: int transport_refs_pushed(struct ref *ref)
      }
      
     -void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
    -+static void update_tracking_ref(struct remote *remote, char *refname,
    -+				struct object_id *new_oid, int deletion,
    -+				int verbose)
    ++static void update_one_tracking_ref(struct remote *remote, char *refname,
    ++				    struct object_id *new_oid, int deletion,
    ++				    int verbose)
      {
      	struct refspec_item rs;
      
     -	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
     -		return;
     -
    + 	memset(&rs, 0, sizeof(rs));
     -	rs.src = ref->name;
     +	rs.src = refname;
      	rs.dst = NULL;
    --
    + 
      	if (!remote_find_tracking(remote, &rs)) {
      		if (verbose)
      			fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst);
    @@ transport.c: int transport_refs_pushed(struct ref *ref)
      			delete_ref(NULL, rs.dst, NULL, 0);
     -		} else
     -			update_ref("update by push", rs.dst, &ref->new_oid,
    --				   NULL, 0, 0);
     +		else
     +			update_ref("update by push", rs.dst, new_oid,
    -+				NULL, 0, 0);
    + 				   NULL, 0, 0);
      		free(rs.dst);
      	}
      }
    @@ transport.c: int transport_refs_pushed(struct ref *ref)
     +
     +	report = ref->report;
     +	if (!report)
    -+		update_tracking_ref(remote, ref->name, &ref->new_oid,
    -+				    ref->deletion, verbose);
    ++		update_one_tracking_ref(remote, ref->name, &ref->new_oid,
    ++					ref->deletion, verbose);
     +	else
     +		for (; report; report = report->next) {
     +			refname = report->ref_name ? (char *)report->ref_name : ref->name;
     +			new_oid = report->new_oid ? report->new_oid : &ref->new_oid;
    -+			update_tracking_ref(remote, refname, new_oid,
    -+					    is_null_oid(new_oid), verbose);
    ++			update_one_tracking_ref(remote, refname, new_oid,
    ++						is_null_oid(new_oid), verbose);
     +		}
     +}
     +
10:  e66c7c3f51 ! 10:  a7268305e3 doc: add documentation for the proc-receive hook
    @@ Documentation/githooks.txt: The default 'update' hook, when enabled--and with
     +    H: PKT-LINE(ok <ref>)
     +    H: PKT-LINE(option fall-through)
     +    # OK, but has an alternate reference.  The alternate reference name
    -+    # and other status can be given in options
    ++    # and other status can be given in option directives.
     +    H: PKT-LINE(ok <ref>)
     +    H: PKT-LINE(option refname <refname>)
     +    H: PKT-LINE(option old-oid <old-oid>)
    @@ Documentation/githooks.txt: The default 'update' hook, when enabled--and with
     +may update an alternate reference and the alternate reference may exist
     +already with a non-zero old-oid.  For this case, this hook will use
     +"option" directives to report extended attributes for the reference given
    -+by the above "ok/ng" directive.
    ++by the leading "ok" directive.
     +
     +The report of the commands of this hook should have the same order as
     +the input.  The exit status of the 'proc-receive' hook only determines

---

Jiang Xin (10):
  transport: not report a non-head push as a branch
  t5411: add basic test cases for proc-receive hook
  receive-pack: add new proc-receive hook
  receive-pack: feed report options to post-receive
  New capability "report-status-v2" for git-push
  doc: add document for capability report-status-v2
  receive-pack: new config receive.procReceiveRefs
  t5411: test updates of remote-tracking branches
  transport: parse report options for tracking refs
  doc: add documentation for the proc-receive hook

 Documentation/config/receive.txt              |  22 +
 Documentation/githooks.txt                    |  62 +++
 Documentation/technical/pack-protocol.txt     |  41 +-
 .../technical/protocol-capabilities.txt       |  17 +-
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 472 +++++++++++++++++-
 builtin/send-pack.c                           |  19 +
 remote.h                                      |   9 +
 send-pack.c                                   | 101 +++-
 t/helper/test-proc-receive.c                  | 176 +++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411-proc-receive-hook.sh                  | 117 +++++
 t/t5411/common-functions.sh                   |  56 +++
 t/t5411/once-0010-report-status-v1.sh         |  94 ++++
 t/t5411/test-0000-standard-git-push.sh        | 143 ++++++
 .../test-0001-standard-git-push--porcelain.sh | 147 ++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0011-no-hook-error.sh            |  64 +++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 +++
 t/t5411/test-0013-bad-protocol.sh             | 217 ++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++
 t/t5411/test-0020-report-ng.sh                |  67 +++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 +++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 ++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 ++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 ++
 t/t5411/test-0026-push-options.sh             |  79 +++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 +++
 t/t5411/test-0030-report-ok.sh                |  35 ++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 ++
 t/t5411/test-0032-report-with-options.sh      | 256 ++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 ++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 ++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 ++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 227 +++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 172 +++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 ++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 ++++
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 +++++
 ...t-0050-proc-receive-refs-with-modifiers.sh | 135 +++++
 t/t5516-fetch-push.sh                         |   2 +-
 transport-helper.c                            | 128 +++--
 transport.c                                   | 156 ++++--
 48 files changed, 4325 insertions(+), 100 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/once-0010-report-status-v1.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh
 create mode 100644 t/t5411/test-0050-proc-receive-refs-with-modifiers.sh

-- 
2.26.2.543.g44b58e439b


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v19 01/10] transport: not report a non-head push as a branch
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 " Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
                                                               ` (8 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a new reference (not a head or tag), report it as a new
reference instead of a new branch.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5516-fetch-push.sh | 2 +-
 transport.c           | 9 ++++++---
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 36ad20a849..d11382f769 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1039,7 +1039,7 @@ test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"
 test_expect_success 'push --porcelain' '
 	mk_empty testrepo &&
 	echo >.git/foo  "To testrepo" &&
-	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new branch]"  &&
+	echo >>.git/foo "*	refs/heads/master:refs/remotes/origin/master	[new reference]"  &&
 	echo >>.git/foo "Done" &&
 	git push >.git/bar --porcelain  testrepo refs/heads/master:refs/remotes/origin/master &&
 	(
diff --git a/transport.c b/transport.c
index 419be0b6ea..84ec80447b 100644
--- a/transport.c
+++ b/transport.c
@@ -500,9 +500,12 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 				 porcelain, summary_width);
 	else if (is_null_oid(&ref->old_oid))
 		print_ref_status('*',
-			(starts_with(ref->name, "refs/tags/") ? "[new tag]" :
-			"[new branch]"),
-			ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 (starts_with(ref->name, "refs/tags/")
+				  ? "[new tag]"
+				  : (starts_with(ref->name, "refs/heads/")
+				     ? "[new branch]"
+				     : "[new reference]")),
+				 ref, ref->peer_ref, NULL, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 02/10] t5411: add basic test cases for proc-receive hook
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 " Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 01/10] transport: not report a non-head push as a branch Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 03/10] receive-pack: add new " Jiang Xin
                                                               ` (7 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Topic "proc-receive-hook" will change the workflow and output of
git-push. Add some basic test cases in t5411 before introducing the new
topic.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411-proc-receive-hook.sh                  | 108 +++++++++++++
 t/t5411/common-functions.sh                   |  56 +++++++
 t/t5411/test-0000-standard-git-push.sh        | 143 +++++++++++++++++
 .../test-0001-standard-git-push--porcelain.sh | 147 ++++++++++++++++++
 t/t5411/test-0002-pre-receive-declined.sh     |  33 ++++
 ...st-0003-pre-receive-declined--porcelain.sh |  34 ++++
 6 files changed, 521 insertions(+)
 create mode 100755 t/t5411-proc-receive-hook.sh
 create mode 100644 t/t5411/common-functions.sh
 create mode 100644 t/t5411/test-0000-standard-git-push.sh
 create mode 100644 t/t5411/test-0001-standard-git-push--porcelain.sh
 create mode 100644 t/t5411/test-0002-pre-receive-declined.sh
 create mode 100644 t/t5411/test-0003-pre-receive-declined--porcelain.sh

diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
new file mode 100755
index 0000000000..3a684353a8
--- /dev/null
+++ b/t/t5411-proc-receive-hook.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+#
+# Copyright (c) 2020 Jiang Xin
+#
+
+test_description='Test proc-receive hook'
+
+. ./test-lib.sh
+
+. "$TEST_DIRECTORY"/t5411/common-functions.sh
+
+setup_upstream_and_workbench () {
+	# Refs of upstream : master(A)
+	# Refs of workbench: master(A)  tags/v123
+	test_expect_success "setup upstream and workbench" '
+		rm -rf upstream.git &&
+		rm -rf workbench &&
+		git init --bare upstream.git &&
+		git init workbench &&
+		create_commits_in workbench A B &&
+		(
+			cd workbench &&
+			# Try to make a stable fixed width for abbreviated commit ID,
+			# this fixed-width oid will be replaced with "<OID>".
+			git config core.abbrev 7 &&
+			git tag -m "v123" v123 $A &&
+			git remote add origin ../upstream.git &&
+			git push origin master &&
+			git update-ref refs/heads/master $A $B &&
+			git -C ../upstream.git update-ref \
+				refs/heads/master $A $B
+		) &&
+		TAG=$(git -C workbench rev-parse v123) &&
+
+		# setup pre-receive hook
+		write_script upstream.git/hooks/pre-receive <<-\EOF &&
+		exec >&2
+		echo "# pre-receive hook"
+		while read old new ref
+		do
+			echo "pre-receive< $old $new $ref"
+		done
+		EOF
+
+		# setup post-receive hook
+		write_script upstream.git/hooks/post-receive <<-\EOF &&
+		exec >&2
+		echo "# post-receive hook"
+		while read old new ref
+		do
+			echo "post-receive< $old $new $ref"
+		done
+		EOF
+
+		upstream=upstream.git
+	'
+}
+
+run_proc_receive_hook_test() {
+	case $1 in
+	http)
+		PROTOCOL="HTTP protocol"
+		URL_PREFIX="http://.*"
+		;;
+	local)
+		PROTOCOL="builtin protocol"
+		URL_PREFIX="\.\."
+		;;
+	esac
+
+	# Include test cases for both file and HTTP protocol
+	for t in  "$TEST_DIRECTORY"/t5411/test-*.sh
+	do
+		. "$t"
+	done
+}
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Run test cases for 'proc-receive' hook on local file protocol.
+run_proc_receive_hook_test local
+
+ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
+. "$TEST_DIRECTORY"/lib-httpd.sh
+. "$TEST_DIRECTORY"/lib-terminal.sh
+start_httpd
+
+# Re-initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup for HTTP protocol" '
+	git -C upstream.git config http.receivepack true &&
+	upstream="$HTTPD_DOCUMENT_ROOT_PATH/upstream.git" &&
+	mv upstream.git "$upstream" &&
+	git -C workbench remote set-url origin "$HTTPD_URL/auth-push/smart/upstream.git" &&
+	set_askpass user@host pass@host
+'
+
+setup_askpass_helper
+
+# Run test cases for 'proc-receive' hook on HTTP protocol.
+run_proc_receive_hook_test http
+
+test_done
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
new file mode 100644
index 0000000000..6580bebd8e
--- /dev/null
+++ b/t/t5411/common-functions.sh
@@ -0,0 +1,56 @@
+# Create commits in <repo> and assign each commit's oid to shell variables
+# given in the arguments (A, B, and C). E.g.:
+#
+#     create_commits_in <repo> A B C
+#
+# NOTE: Never calling this function from a subshell since variable
+# assignments will disappear when subshell exits.
+create_commits_in () {
+	repo="$1" &&
+	if ! parent=$(git -C "$repo" rev-parse HEAD^{} --)
+	then
+		parent=
+	fi &&
+	T=$(git -C "$repo" write-tree) &&
+	shift &&
+	while test $# -gt 0
+	do
+		name=$1 &&
+		test_tick &&
+		if test -z "$parent"
+		then
+			oid=$(echo $name | git -C "$repo" commit-tree $T)
+		else
+			oid=$(echo $name | git -C "$repo" commit-tree -p $parent $T)
+		fi &&
+		eval $name=$oid &&
+		parent=$oid &&
+		shift ||
+		return 1
+	done &&
+	git -C "$repo" update-ref refs/heads/master $oid
+}
+
+# Format the output of git-push, git-show-ref and other commands to make a
+# user-friendly and stable text.  We can easily prepare the expect text
+# without having to worry about future changes of the commit ID and spaces
+# of the output.  Single quotes are replaced with double quotes, because
+# it is boring to prepare unquoted single quotes in expect text.  We also
+# remove some locale error messages, which break test if we turn on
+# `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
+# on plumbing commands.
+make_user_friendly_and_stable_output () {
+	sed \
+		-e "s/  *\$//" \
+		-e "s/   */ /g" \
+		-e "s/'/\"/g" \
+		-e "s/	/    /g" \
+		-e "s/$A/<COMMIT-A>/g" \
+		-e "s/$B/<COMMIT-B>/g" \
+		-e "s/$TAG/<TAG-v123>/g" \
+		-e "s/$ZERO_OID/<ZERO-OID>/g" \
+		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
+		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
+		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
+		-e "/^error: / d"
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
new file mode 100644
index 0000000000..e206587348
--- /dev/null
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -0,0 +1,143 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> next
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [rejected] master -> master (non-fast-forward)
+	 ! [rejected] <COMMIT-B> -> next (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> next
+	 ! [rejected] master -> master (non-fast-forward)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	 + <OID-B>...<OID-A> master -> master (forced update)
+	 - [deleted] next
+	 * [new tag] v123 -> v123
+	 * [new reference] master -> refs/review/master/topic
+	 * [new branch] HEAD -> a/b/c
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
new file mode 100644
index 0000000000..48f6fcc846
--- /dev/null
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -0,0 +1,147 @@
+# Refs of upstream : master(A)  
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/next    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push --atomic: master(A)  next(B)
+test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --atomic --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "s/^# GETTEXT POISON #//" \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	! refs/heads/master:refs/heads/master [rejected] (non-fast-forward)
+	! <COMMIT-B>:refs/heads/next [rejected] (atomic push failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(A)
+# Refs of workbench: master(A)           tags/v123
+# git-push         : master(A)  next(B)
+test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
+	test_must_fail git \
+		-C workbench \
+		-c advice.pushUpdateRejected=false \
+		push --porcelain origin \
+		master \
+		$B:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/next    <OID-A>..<OID-B>
+	!    refs/heads/master:refs/heads/master    [rejected] (non-fast-forward)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	<COMMIT-B> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)  next(B)
+# Refs of workbench: master(A)           tags/v123
+# git-push -f      : master(A)  NULL     tags/v123  refs/review/master/topic(A)  a/b/c(A)
+test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		refs/tags/v123 \
+		:refs/heads/next \
+		master \
+		master:refs/review/master/topic \
+		HEAD:refs/heads/a/b/c \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-B> <ZERO-OID> refs/heads/next
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/review/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/a/b/c
+	To <URL/of/upstream.git>
+	+    refs/heads/master:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	-    :refs/heads/next    [deleted]
+	*    refs/tags/v123:refs/tags/v123    [new tag]
+	*    refs/heads/master:refs/review/master/topic    [new reference]
+	*    HEAD:refs/heads/a/b/c    [new branch]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/a/b/c
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/review/master/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)  tags/v123  refs/review/master/topic(A)  a/b/c(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/review/master/topic &&
+		git update-ref -d refs/tags/v123 &&
+		git update-ref -d refs/heads/a/b/c
+	)
+'
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
new file mode 100644
index 0000000000..c246f7e68e
--- /dev/null
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -0,0 +1,33 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (pre-receive hook declined)
+	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
new file mode 100644
index 0000000000..b14894de81
--- /dev/null
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup pre-receive hook ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive" "$upstream/hooks/pre-receive.ok" &&
+	write_script "$upstream/hooks/pre-receive" <<-EOF
+	exit 1
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git-push         : master(B)             next(A)
+test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/next \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (pre-receive hook declined)
+	!    HEAD:refs/heads/next    [remote rejected] (pre-receive hook declined)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	mv "$upstream/hooks/pre-receive.ok" "$upstream/hooks/pre-receive"
+'
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (2 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-11-04 22:15                                               ` Johannes Schindelin
  2021-01-17 22:21                                               ` SZEDER Gábor
  2020-08-27 15:45                                             ` [PATCH v19 04/10] receive-pack: feed report options to post-receive Jiang Xin
                                                               ` (6 subsequent siblings)
  10 siblings, 2 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Git calls an internal `execute_commands` function to handle commands
sent from client to `git-receive-pack`.  Regardless of what references
the user pushes, git creates or updates the corresponding references if
the user has write-permission.  A contributor who has no
write-permission, cannot push to the repository directly.  So, the
contributor has to write commits to an alternate location, and sends
pull request by emails or by other ways.  We call this workflow as a
distributed workflow.

It would be more convenient to work in a centralized workflow like what
Gerrit provided for some cases.  For example, a read-only user who
cannot push to a branch directly can run the following `git push`
command to push commits to a pseudo reference (has a prefix "refs/for/",
not "refs/heads/") to create a code review.

    git push origin \
        HEAD:refs/for/<branch-name>/<session>

The `<branch-name>` in the above example can be as simple as "master",
or a more complicated branch name like "foo/bar".  The `<session>` in
the above example command can be the local branch name of the client
side, such as "my/topic".

We cannot implement a centralized workflow elegantly by using
"pre-receive" + "post-receive", because Git will call the internal
function "execute_commands" to create references (even the special
pseudo reference) between these two hooks.  Even though we can delete
the temporarily created pseudo reference via the "post-receive" hook,
having a temporary reference is not safe for concurrent pushes.

So, add a filter and a new handler to support this kind of workflow.
The filter will check the prefix of the reference name, and if the
command has a special reference name, the filter will turn a specific
field (`run_proc_receive`) on for the command.  Commands with this filed
turned on will be executed by a new handler (a hook named
"proc-receive") instead of the internal `execute_commands` function.
We can use this "proc-receive" command to create pull requests or send
emails for code review.

Suggested by Junio, this "proc-receive" hook reads the commands,
push-options (optional), and send result using a protocol in pkt-line
format.  In the following example, the letter "S" stands for
"receive-pack" and letter "H" stands for the hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option fall-through)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status can be given in options
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option refname <refname>)
    H: PKT-LINE(option old-oid <old-oid>)
    H: PKT-LINE(option new-oid <new-oid>)
    H: PKT-LINE(option forced-update)
    H: ... ...
    H: flush-pkt

After receiving a command, the hook will execute the command, and may
create/update different reference.  For example, a command for a pseudo
reference "refs/for/master/topic" may create/update different reference
such as "refs/pull/123/head".  The alternate reference name and other
status are given in option lines.

The list of commands returned from "proc-receive" will replace the
relevant commands that are sent from user to "receive-pack", and
"receive-pack" will continue to run the "execute_commands" function and
other routines.  Finally, the result of the execution of these commands
will be reported to end user.

The reporting function from "receive-pack" to "send-pack" will be
extended in latter commit just like what the "proc-receive" hook reports
to "receive-pack".

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Makefile                                      |   1 +
 builtin/receive-pack.c                        | 296 +++++++++++++++++-
 remote.h                                      |   8 +
 t/helper/test-proc-receive.c                  | 176 +++++++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t5411/test-0011-no-hook-error.sh            |  64 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh |  66 ++++
 t/t5411/test-0013-bad-protocol.sh             | 217 +++++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 160 ++++++++++
 t/t5411/test-0020-report-ng.sh                |  67 ++++
 t/t5411/test-0021-report-ng--porcelain.sh     |  69 ++++
 t/t5411/test-0022-report-unexpect-ref.sh      |  45 +++
 ...est-0023-report-unexpect-ref--porcelain.sh |  46 +++
 t/t5411/test-0024-report-unknown-ref.sh       |  34 ++
 ...test-0025-report-unknown-ref--porcelain.sh |  35 +++
 t/t5411/test-0026-push-options.sh             |  79 +++++
 t/t5411/test-0027-push-options--porcelain.sh  |  82 +++++
 t/t5411/test-0030-report-ok.sh                |  35 +++
 t/t5411/test-0031-report-ok--porcelain.sh     |  36 +++
 t/t5411/test-0032-report-with-options.sh      | 256 +++++++++++++++
 ...est-0033-report-with-options--porcelain.sh | 265 ++++++++++++++++
 t/t5411/test-0034-report-ft.sh                |  44 +++
 t/t5411/test-0035-report-ft--porcelain.sh     |  45 +++
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 159 ++++++++++
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 162 ++++++++++
 t/t5411/test-0038-report-mixed-refs.sh        |  89 ++++++
 .../test-0039-report-mixed-refs--porcelain.sh |  91 ++++++
 28 files changed, 2626 insertions(+), 3 deletions(-)
 create mode 100644 t/helper/test-proc-receive.c
 create mode 100644 t/t5411/test-0011-no-hook-error.sh
 create mode 100644 t/t5411/test-0012-no-hook-error--porcelain.sh
 create mode 100644 t/t5411/test-0013-bad-protocol.sh
 create mode 100644 t/t5411/test-0014-bad-protocol--porcelain.sh
 create mode 100644 t/t5411/test-0020-report-ng.sh
 create mode 100644 t/t5411/test-0021-report-ng--porcelain.sh
 create mode 100644 t/t5411/test-0022-report-unexpect-ref.sh
 create mode 100644 t/t5411/test-0023-report-unexpect-ref--porcelain.sh
 create mode 100644 t/t5411/test-0024-report-unknown-ref.sh
 create mode 100644 t/t5411/test-0025-report-unknown-ref--porcelain.sh
 create mode 100644 t/t5411/test-0026-push-options.sh
 create mode 100644 t/t5411/test-0027-push-options--porcelain.sh
 create mode 100644 t/t5411/test-0030-report-ok.sh
 create mode 100644 t/t5411/test-0031-report-ok--porcelain.sh
 create mode 100644 t/t5411/test-0032-report-with-options.sh
 create mode 100644 t/t5411/test-0033-report-with-options--porcelain.sh
 create mode 100644 t/t5411/test-0034-report-ft.sh
 create mode 100644 t/t5411/test-0035-report-ft--porcelain.sh
 create mode 100644 t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
 create mode 100644 t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
 create mode 100644 t/t5411/test-0038-report-mixed-refs.sh
 create mode 100644 t/t5411/test-0039-report-mixed-refs--porcelain.sh

diff --git a/Makefile b/Makefile
index 65f8cfb236..48c048584e 100644
--- a/Makefile
+++ b/Makefile
@@ -719,6 +719,7 @@ TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
 TEST_BUILTINS_OBJS += test-path-utils.o
 TEST_BUILTINS_OBJS += test-pkt-line.o
 TEST_BUILTINS_OBJS += test-prio-queue.o
+TEST_BUILTINS_OBJS += test-proc-receive.o
 TEST_BUILTINS_OBJS += test-progress.o
 TEST_BUILTINS_OBJS += test-reach.o
 TEST_BUILTINS_OBJS += test-read-cache.o
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 439f29d6c7..d4614f6f67 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -310,11 +310,15 @@ static void write_head_info(void)
 	packet_flush(1);
 }
 
+#define RUN_PROC_RECEIVE_SCHEDULED	1
+#define RUN_PROC_RECEIVE_RETURNED	2
 struct command {
 	struct command *next;
 	const char *error_string;
+	struct ref_push_report *report;
 	unsigned int skip_update:1,
-		     did_not_exist:1;
+		     did_not_exist:1,
+		     run_proc_receive:2;
 	int index;
 	struct object_id old_oid;
 	struct object_id new_oid;
@@ -840,6 +844,268 @@ static int run_update_hook(struct command *cmd)
 	return finish_command(&proc);
 }
 
+static struct command *find_command_by_refname(struct command *list,
+					       const char *refname)
+{
+	for (; list; list = list->next)
+		if (!strcmp(list->ref_name, refname))
+			return list;
+	return NULL;
+}
+
+static int read_proc_receive_report(struct packet_reader *reader,
+				    struct command *commands,
+				    struct strbuf *errmsg)
+{
+	struct command *cmd;
+	struct command *hint = NULL;
+	struct ref_push_report *report = NULL;
+	int new_report = 0;
+	int code = 0;
+	int once = 0;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		const char *head;
+		const char *refname;
+		char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head);
+			code = -1;
+			continue;
+		}
+		*p++ = '\0';
+		if (!strcmp(head, "option")) {
+			const char *key, *val;
+
+			if (!hint || !(report || new_report)) {
+				if (!once++)
+					strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n");
+				code = -1;
+				continue;
+			}
+			if (new_report) {
+				if (!hint->report) {
+					hint->report = xcalloc(1, sizeof(struct ref_push_report));
+					report = hint->report;
+				} else {
+					report = hint->report;
+					while (report->next)
+						report = report->next;
+					report->next = xcalloc(1, sizeof(struct ref_push_report));
+					report = report->next;
+				}
+				new_report = 0;
+			}
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				report->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				report->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				report->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				report->forced_update = 1;
+			else if (!strcmp(key, "fall-through"))
+				/* Fall through, let 'receive-pack' to execute it. */
+				hint->run_proc_receive = 0;
+			continue;
+		}
+
+		report = NULL;
+		new_report = 0;
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n",
+				    head, refname);
+			code = -1;
+			continue;
+		}
+
+		/* first try searching at our hint, falling back to all refs */
+		if (hint)
+			hint = find_command_by_refname(hint, refname);
+		if (!hint)
+			hint = find_command_by_refname(commands, refname);
+		if (!hint) {
+			strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		if (!hint->run_proc_receive) {
+			strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n",
+				    refname);
+			code = -1;
+			continue;
+		}
+		hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED;
+		if (!strcmp(head, "ng")) {
+			if (p)
+				hint->error_string = xstrdup(p);
+			else
+				hint->error_string = "failed";
+			code = -1;
+			continue;
+		}
+		new_report = 1;
+	}
+
+	for (cmd = commands; cmd; cmd = cmd->next)
+		if (cmd->run_proc_receive && !cmd->error_string &&
+		    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) {
+		    cmd->error_string = "proc-receive failed to report status";
+		    code = -1;
+		}
+	return code;
+}
+
+static int run_proc_receive_hook(struct command *commands,
+				 const struct string_list *push_options)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct async muxer;
+	struct command *cmd;
+	const char *argv[2];
+	struct packet_reader reader;
+	struct strbuf cap = STRBUF_INIT;
+	struct strbuf errmsg = STRBUF_INIT;
+	int hook_use_push_options = 0;
+	int version = 0;
+	int code;
+
+	argv[0] = find_hook("proc-receive");
+	if (!argv[0]) {
+		rp_error("cannot find hook 'proc-receive'");
+		return -1;
+	}
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.out = -1;
+	proc.trace2_hook_name = "proc-receive";
+
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		code = start_async(&muxer);
+		if (code)
+			return code;
+		proc.err = muxer.in;
+	} else {
+		proc.err = 0;
+	}
+
+	code = start_command(&proc);
+	if (code) {
+		if (use_sideband)
+			finish_async(&muxer);
+		return code;
+	}
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	/* Version negotiaton */
+	packet_reader_init(&reader, proc.out, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF);
+	if (use_atomic)
+		strbuf_addstr(&cap, " atomic");
+	if (use_push_options)
+		strbuf_addstr(&cap, " push-options");
+	if (cap.len) {
+		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		strbuf_release(&cap);
+	} else {
+		packet_write_fmt(proc.in, "version=1\n");
+	}
+	packet_flush(proc.in);
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+			version = atoi(reader.line + 8);
+			linelen = strlen(reader.line);
+			if (linelen < reader.pktlen) {
+				const char *feature_list = reader.line + linelen + 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					hook_use_push_options = 1;
+			}
+		}
+	}
+
+	if (version != 1) {
+		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
+			    version);
+		code = -1;
+		goto cleanup;
+	}
+
+	/* Send commands */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
+			continue;
+		packet_write_fmt(proc.in, "%s %s %s",
+				 oid_to_hex(&cmd->old_oid),
+				 oid_to_hex(&cmd->new_oid),
+				 cmd->ref_name);
+	}
+	packet_flush(proc.in);
+
+	/* Send push options */
+	if (hook_use_push_options) {
+		struct string_list_item *item;
+
+		for_each_string_list_item(item, push_options)
+			packet_write_fmt(proc.in, "%s", item->string);
+		packet_flush(proc.in);
+	}
+
+	/* Read result from proc-receive */
+	code = read_proc_receive_report(&reader, commands, &errmsg);
+
+cleanup:
+	close(proc.in);
+	close(proc.out);
+	if (use_sideband)
+		finish_async(&muxer);
+	if (finish_command(&proc))
+		code = -1;
+	if (errmsg.len >0) {
+		char *p = errmsg.buf;
+
+		p += errmsg.len - 1;
+		if (*p == '\n')
+			*p = '\0';
+		rp_error("%s", errmsg.buf);
+		strbuf_release(&errmsg);
+	}
+	sigchain_pop(SIGPIPE);
+
+	return code;
+}
+
 static char *refuse_unconfigured_deny_msg =
 	N_("By default, updating the current branch in a non-bare repository\n"
 	   "is denied, because it will make the index and work tree inconsistent\n"
@@ -1415,7 +1681,7 @@ static void execute_commands_non_atomic(struct command *commands,
 	struct strbuf err = STRBUF_INIT;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		transaction = ref_transaction_begin(&err);
@@ -1455,7 +1721,7 @@ static void execute_commands_atomic(struct command *commands,
 	}
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
+		if (!should_process_cmd(cmd) || cmd->run_proc_receive)
 			continue;
 
 		cmd->error_string = update(cmd, si);
@@ -1491,6 +1757,7 @@ static void execute_commands(struct command *commands,
 	struct iterate_data data;
 	struct async muxer;
 	int err_fd = 0;
+	int run_proc_receive = 0;
 
 	if (unpacker_error) {
 		for (cmd = commands; cmd; cmd = cmd->next)
@@ -1520,6 +1787,21 @@ static void execute_commands(struct command *commands,
 
 	reject_updates_to_hidden(commands);
 
+	/*
+	 * Try to find commands that have special prefix in their reference names,
+	 * and mark them to run an external "proc-receive" hook later.
+	 */
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		if (!should_process_cmd(cmd))
+			continue;
+
+		/* TODO: replace the fixed prefix by looking up git config variables. */
+		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
+			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+			run_proc_receive = 1;
+		}
+	}
+
 	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
@@ -1546,6 +1828,14 @@ static void execute_commands(struct command *commands,
 	free(head_name_to_free);
 	head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL);
 
+	if (run_proc_receive &&
+	    run_proc_receive_hook(commands, push_options))
+		for (cmd = commands; cmd; cmd = cmd->next)
+			if (!cmd->error_string &&
+			    !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED) &&
+			    (cmd->run_proc_receive || use_atomic))
+				cmd->error_string = "fail to run proc-receive hook";
+
 	if (use_atomic)
 		execute_commands_atomic(commands, si);
 	else
diff --git a/remote.h b/remote.h
index 5e3ea5a26d..519349333a 100644
--- a/remote.h
+++ b/remote.h
@@ -93,6 +93,14 @@ int for_each_remote(each_remote_fn fn, void *priv);
 
 int remote_has_url(struct remote *remote, const char *url);
 
+struct ref_push_report {
+	const char *ref_name;
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	unsigned int forced_update:1;
+	struct ref_push_report *next;
+};
+
 struct ref {
 	struct ref *next;
 	struct object_id old_oid;
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
new file mode 100644
index 0000000000..42164d9898
--- /dev/null
+++ b/t/helper/test-proc-receive.c
@@ -0,0 +1,176 @@
+#include "cache.h"
+#include "connect.h"
+#include "parse-options.h"
+#include "pkt-line.h"
+#include "sigchain.h"
+#include "test-tool.h"
+
+static const char *proc_receive_usage[] = {
+	"test-tool proc-receive [<options>...]",
+	NULL
+};
+
+static int die_version;
+static int die_readline;
+static int no_push_options;
+static int use_atomic;
+static int use_push_options;
+static int verbose;
+static int version = 1;
+static struct string_list returns = STRING_LIST_INIT_NODUP;
+
+struct command {
+	struct command *next;
+	const char *error_string;
+	unsigned int skip_update:1,
+		     did_not_exist:1;
+	int index;
+	struct object_id old_oid;
+	struct object_id new_oid;
+	char ref_name[FLEX_ARRAY]; /* more */
+};
+
+static void proc_receive_verison(struct packet_reader *reader) {
+	int server_version = 0;
+
+	for (;;) {
+		int linelen;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
+			server_version = atoi(reader->line+8);
+			linelen = strlen(reader->line);
+			if (linelen < reader->pktlen) {
+				const char *feature_list = reader->line + linelen + 1;
+				if (parse_feature_request(feature_list, "atomic"))
+					use_atomic= 1;
+				if (parse_feature_request(feature_list, "push-options"))
+					use_push_options = 1;
+			}
+		}
+	}
+
+	if (server_version != 1 || die_version)
+		die("bad protocol version: %d", server_version);
+
+	packet_write_fmt(1, "version=%d%c%s\n",
+			 version, '\0',
+			 use_push_options && !no_push_options ? "push-options": "");
+	packet_flush(1);
+}
+
+static void proc_receive_read_commands(struct packet_reader *reader,
+				       struct command **commands)
+{
+	struct command **tail = commands;
+
+	for (;;) {
+		struct object_id old_oid, new_oid;
+		struct command *cmd;
+		const char *refname;
+		const char *p;
+
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		if (parse_oid_hex(reader->line, &old_oid, &p) ||
+		    *p++ != ' ' ||
+		    parse_oid_hex(p, &new_oid, &p) ||
+		    *p++ != ' ' ||
+		    die_readline)
+			die("protocol error: expected 'old new ref', got '%s'",
+			    reader->line);
+		refname = p;
+		FLEX_ALLOC_STR(cmd, ref_name, refname);
+		oidcpy(&cmd->old_oid, &old_oid);
+		oidcpy(&cmd->new_oid, &new_oid);
+
+		*tail = cmd;
+		tail = &cmd->next;
+	}
+}
+
+static void proc_receive_read_push_options(struct packet_reader *reader,
+					   struct string_list *options)
+{
+
+	if (no_push_options || !use_push_options)
+	       return;
+
+	while (1) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+			break;
+
+		string_list_append(options, reader->line);
+	}
+}
+
+int cmd__proc_receive(int argc, const char **argv)
+{
+	int nongit_ok = 0;
+	struct packet_reader reader;
+	struct command *commands = NULL;
+	struct string_list push_options = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct option options[] = {
+		OPT_BOOL(0, "no-push-options", &no_push_options,
+			 "disable push options"),
+		OPT_BOOL(0, "die-version", &die_version,
+			 "die during version negotiation"),
+		OPT_BOOL(0, "die-readline", &die_readline,
+			 "die when readline"),
+		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
+				"return of results"),
+		OPT__VERBOSE(&verbose, "be verbose"),
+		OPT_INTEGER('V', "version", &version,
+			    "use this protocol version number"),
+		OPT_END()
+	};
+
+	setup_git_directory_gently(&nongit_ok);
+
+	argc = parse_options(argc, argv, "test-tools", options, proc_receive_usage, 0);
+	if (argc > 0)
+		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
+	packet_reader_init(&reader, 0, NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	proc_receive_verison(&reader);
+	proc_receive_read_commands(&reader, &commands);
+	proc_receive_read_push_options(&reader, &push_options);
+
+	if (verbose) {
+		struct command *cmd;
+
+		if (use_push_options || use_atomic)
+			fprintf(stderr, "proc-receive:%s%s\n",
+				use_atomic? " atomic": "",
+				use_push_options ? " push_options": "");
+
+		for (cmd = commands; cmd; cmd = cmd->next)
+			fprintf(stderr, "proc-receive< %s %s %s\n",
+				oid_to_hex(&cmd->old_oid),
+				oid_to_hex(&cmd->new_oid),
+				cmd->ref_name);
+
+		if (push_options.nr > 0)
+			for_each_string_list_item(item, &push_options)
+				fprintf(stderr, "proc-receive< %s\n", item->string);
+
+		if (returns.nr)
+			for_each_string_list_item(item, &returns)
+				fprintf(stderr, "proc-receive> %s\n", item->string);
+	}
+
+	if (returns.nr)
+		for_each_string_list_item(item, &returns)
+			packet_write_fmt(1, "%s\n", item->string);
+	packet_flush(1);
+	sigchain_pop(SIGPIPE);
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 590b2efca7..a0d3966b29 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -46,6 +46,7 @@ static struct test_cmd cmds[] = {
 	{ "path-utils", cmd__path_utils },
 	{ "pkt-line", cmd__pkt_line },
 	{ "prio-queue", cmd__prio_queue },
+	{ "proc-receive", cmd__proc_receive},
 	{ "progress", cmd__progress },
 	{ "reach", cmd__reach },
 	{ "read-cache", cmd__read_cache },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index ddc8e990e9..07034d3f38 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -35,6 +35,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
 int cmd__path_utils(int argc, const char **argv);
 int cmd__pkt_line(int argc, const char **argv);
 int cmd__prio_queue(int argc, const char **argv);
+int cmd__proc_receive(int argc, const char **argv);
 int cmd__progress(int argc, const char **argv);
 int cmd__reach(int argc, const char **argv);
 int cmd__read_cache(int argc, const char **argv);
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
new file mode 100644
index 0000000000..bb6ec92a92
--- /dev/null
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -0,0 +1,64 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL)" '
+	test_must_fail git -C workbench push --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] <COMMIT-B> -> master (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> next (fail to run proc-receive hook)
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
new file mode 100644
index 0000000000..4814f74dc2
--- /dev/null
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -0,0 +1,66 @@
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push --atomic: (B)                   next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain --atomic origin \
+		$B:master \
+		HEAD:next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: error: cannot find hook "proc-receive"
+	To <URL/of/upstream.git>
+	!    <COMMIT-B>:refs/heads/master    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/heads/next    [remote rejected] (fail to run proc-receive hook)
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
new file mode 100644
index 0000000000..c5fe4cb37b
--- /dev/null
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -0,0 +1,217 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: fatal: bad protocol version: 1
+	remote: error: proc-receive version "0" is not supported
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
new file mode 100644
index 0000000000..53b47b0185
--- /dev/null
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -0,0 +1,160 @@
+test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --version 2
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+
+	# Check status report for git-push
+	sed -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<actual >actual-report &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual-report &&
+
+	# Check error message from "receive-pack", but ignore unstable fatal error
+	# message ("remote: fatal: the remote end hung up unexpectedly") which
+	# is different from the remote HTTP server with different locale settings.
+	grep "^remote: error:" <actual >actual-error &&
+	cat >expect <<-EOF &&
+	remote: error: proc-receive version "2" is not supported
+	EOF
+	test_cmp expect actual-error &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       next(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+
+'
+
+test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic\
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok
+	remote: error: proc-receive reported incomplete status line: "ok"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "xx refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+			HEAD:refs/for/master/topic \
+			>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> xx refs/for/master/topic
+	remote: error: proc-receive reported bad status "xx" on ref "refs/for/master/topic"
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
new file mode 100644
index 0000000000..f726b7ca9c
--- /dev/null
+++ b/t/t5411/test-0020-report-ng.sh
@@ -0,0 +1,67 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (failed)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (error msg)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
new file mode 100644
index 0000000000..fbf5569103
--- /dev/null
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -0,0 +1,69 @@
+test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (failed)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ng refs/for/master/topic error msg"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ng refs/for/master/topic error msg
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (error msg)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
new file mode 100644
index 0000000000..92a415b929
--- /dev/null
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
new file mode 100644
index 0000000000..acbf93e40a
--- /dev/null
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -0,0 +1,46 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic
+test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: error: proc-receive reported status on unexpected ref: refs/heads/master
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref refs/heads/master $A
+'
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
new file mode 100644
index 0000000000..c3946f329a
--- /dev/null
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -0,0 +1,34 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
new file mode 100644
index 0000000000..d093b1a579
--- /dev/null
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/a/b/c/my/topic
+test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/a/b/c/my/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: error: proc-receive reported status on unknown ref: refs/for/master/topic
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/a/b/c/my/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
new file mode 100644
index 0000000000..d0c4da8b23
--- /dev/null
+++ b/t/t5411/test-0026-push-options.sh
@@ -0,0 +1,79 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
+	test_must_fail git -C workbench push \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
new file mode 100644
index 0000000000..c89a1e7c57
--- /dev/null
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -0,0 +1,82 @@
+test_expect_success "setup proc-receive hook and disable push-options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions false &&
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       refs/for/master/topic
+test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push \
+		--porcelain \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	test_i18ngrep "fatal: the receiving end does not support push options" \
+		actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "enable push options ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions true
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/master/topic
+test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive: atomic push_options
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< issue=123
+	remote: proc-receive< reviewer=user1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             next(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
new file mode 100644
index 0000000000..44c99d3831
--- /dev/null
+++ b/t/t5411/test-0030-report-ok.sh
@@ -0,0 +1,35 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
new file mode 100644
index 0000000000..3223b26184
--- /dev/null
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -0,0 +1,36 @@
+test_expect_success "setup proc-receive hook (ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic
+test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
new file mode 100644
index 0000000000..a743aa8018
--- /dev/null
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -0,0 +1,256 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/for/a/b/c/topic
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
new file mode 100644
index 0000000000..439b97b06e
--- /dev/null
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -0,0 +1,265 @@
+test_expect_success "setup proc-receive hook (option without matching ok, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: error: proc-receive reported "option" without a matching "ok/ng" directive
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and forced-update, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and forced-update ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option refname and old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (option old-oid and new-oid, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (report with multiple rewrites, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/a/b/c/topic" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option forced-update" \
+		-r "option new-oid $A"
+
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/next/topic(A)  refs/for/a/b/c/topic(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/next/topic \
+		HEAD:refs/for/a/b/c/topic \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/a/b/c/topic
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option forced-update
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/for/a/b/c/topic    [new reference]
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
new file mode 100644
index 0000000000..aca2b0676c
--- /dev/null
+++ b/t/t5411/test-0034-report-ft.sh
@@ -0,0 +1,44 @@
+test_expect_success "setup proc-receive hook (ft, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] <COMMIT-B> -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
new file mode 100644
index 0000000000..30ffffb352
--- /dev/null
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -0,0 +1,45 @@
+test_expect_success "setup proc-receive hook (fall-through, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option fall-through"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(B)
+test_expect_success "proc-receive: fall throught, let receive-pack to execute ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		$B:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option fall-through
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-B> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    <COMMIT-B>:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/for/master/topic
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             refs/for/master/topic(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/for/master/topic
+'
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
new file mode 100644
index 0000000000..12acf9ea1f
--- /dev/null
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -0,0 +1,159 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
+	git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 * [new reference] HEAD -> refs/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
new file mode 100644
index 0000000000..f4a2c56e14
--- /dev/null
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -0,0 +1,162 @@
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrite for one ref, no refname for the 1st rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/1" \
+		-r "option old-oid $ZERO_OID" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/25/125/1" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref, no refname for the 2nd rewrite ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/1
+	remote: proc-receive> option old-oid <ZERO-OID>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/25/125/1
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/23/123/1" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/changes/24/124/2" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/23/123/1
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/changes/24/124/2
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/for/master/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
new file mode 100644
index 0000000000..1dd6a97a97
--- /dev/null
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -0,0 +1,89 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> master
+	 * [new branch] HEAD -> bar
+	 * [new branch] HEAD -> baz
+	 * [new reference] HEAD -> refs/for/next/topic2
+	 * [new branch] HEAD -> foo
+	 * [new reference] HEAD -> refs/for/master/topic
+	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
+	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
new file mode 100644
index 0000000000..32ebf63dcf
--- /dev/null
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -0,0 +1,91 @@
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/next/topic2" \
+		-r "ng refs/for/next/topic1 fail to call Web API" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/for/master/topic" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   bar(A)  baz(A)  refs/for/next/topic(A)  foo(A)  refs/for/master/topic(A)
+test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		$B:refs/heads/master \
+		HEAD:refs/heads/bar \
+		HEAD:refs/heads/baz \
+		HEAD:refs/for/next/topic2 \
+		HEAD:refs/for/next/topic1 \
+		HEAD:refs/heads/foo \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic3 \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic1
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic3
+	remote: proc-receive> ok refs/for/next/topic2
+	remote: proc-receive> ng refs/for/next/topic1 fail to call Web API
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/for/master/topic
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/bar
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
+	*    HEAD:refs/heads/bar    [new branch]
+	*    HEAD:refs/heads/baz    [new branch]
+	*    HEAD:refs/for/next/topic2    [new reference]
+	*    HEAD:refs/heads/foo    [new branch]
+	*    HEAD:refs/for/master/topic    [new reference]
+	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
+	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $A &&
+		git update-ref -d refs/heads/foo &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+
+'
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 04/10] receive-pack: feed report options to post-receive
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (3 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 03/10] receive-pack: add new " Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 05/10] New capability "report-status-v2" for git-push Jiang Xin
                                                               ` (5 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When commands are fed to the "post-receive" hook, report options will
be parsed and the real old-oid, new-oid, reference name will feed to
the "post-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        | 30 ++++++++++++++++---
 t/t5411/test-0032-report-with-options.sh      | 14 ++++-----
 ...est-0033-report-with-options--porcelain.sh | 14 ++++-----
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 11 +++++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 11 +++++--
 t/t5411/test-0038-report-mixed-refs.sh        |  2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  2 +-
 7 files changed, 58 insertions(+), 26 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index d4614f6f67..0390f2dc97 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -696,6 +696,7 @@ static void prepare_push_cert_sha1(struct child_process *proc)
 
 struct receive_hook_feed_state {
 	struct command *cmd;
+	struct ref_push_report *report;
 	int skip_broken;
 	struct strbuf buf;
 	const struct string_list *push_options;
@@ -783,11 +784,31 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 		cmd = cmd->next;
 	if (!cmd)
 		return -1; /* EOF */
+	if (!bufp)
+		return 0; /* OK, can feed something. */
 	strbuf_reset(&state->buf);
-	strbuf_addf(&state->buf, "%s %s %s\n",
-		    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
-		    cmd->ref_name);
-	state->cmd = cmd->next;
+	if (!state->report)
+		state->report = cmd->report;
+	if (state->report) {
+		struct object_id *old_oid;
+		struct object_id *new_oid;
+		const char *ref_name;
+
+		old_oid = state->report->old_oid ? state->report->old_oid : &cmd->old_oid;
+		new_oid = state->report->new_oid ? state->report->new_oid : &cmd->new_oid;
+		ref_name = state->report->ref_name ? state->report->ref_name : cmd->ref_name;
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(old_oid), oid_to_hex(new_oid),
+			    ref_name);
+		state->report = state->report->next;
+		if (!state->report)
+			state->cmd = cmd->next;
+	} else {
+		strbuf_addf(&state->buf, "%s %s %s\n",
+			    oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
+			    cmd->ref_name);
+		state->cmd = cmd->next;
+	}
 	if (bufp) {
 		*bufp = state->buf.buf;
 		*sizep = state->buf.len;
@@ -806,6 +827,7 @@ static int run_receive_hook(struct command *commands,
 	strbuf_init(&state.buf, 0);
 	state.cmd = commands;
 	state.skip_broken = skip_broken;
+	state.report = NULL;
 	if (feed_receive_hook(&state, NULL, NULL))
 		return 0;
 	state.cmd = commands;
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a743aa8018..c559c207fa 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -54,7 +54,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -87,7 +87,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -121,7 +121,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -153,7 +153,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -187,7 +187,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -237,9 +237,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/next/topic
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 439b97b06e..ea9312234e 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -55,7 +55,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -90,7 +90,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -125,7 +125,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: proc-receive> option refname refs/pull/123/head
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -158,7 +158,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: proc-receive> ok refs/for/master/topic
 	remote: proc-receive> option old-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -193,7 +193,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -245,9 +245,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: proc-receive> option forced-update
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/next/topic    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 12acf9ea1f..67cc4a8a4c 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -99,7 +101,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
@@ -145,7 +149,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	 * [new reference] HEAD -> refs/for/master/topic
 	EOF
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index f4a2c56e14..1dc8551bc6 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -41,7 +41,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -100,7 +102,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: proc-receive> option new-oid <COMMIT-A>
 	remote: proc-receive> option forced-update
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
@@ -147,7 +151,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: proc-receive> option old-oid <COMMIT-A>
 	remote: proc-receive> option new-oid <COMMIT-B>
 	remote: # post-receive hook
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
 	*    HEAD:refs/for/master/topic    [new reference]
 	Done
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 1dd6a97a97..bfc8d586d6 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	 <OID-A>..<OID-B> <COMMIT-B> -> master
 	 * [new branch] HEAD -> bar
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 32ebf63dcf..5d021a4837 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -53,7 +53,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/baz
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic2
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
-	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
 	     <COMMIT-B>:refs/heads/master    <OID-A>..<OID-B>
 	*    HEAD:refs/heads/bar    [new branch]
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 05/10] New capability "report-status-v2" for git-push
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (4 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 04/10] receive-pack: feed report options to post-receive Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 06/10] doc: add document for capability report-status-v2 Jiang Xin
                                                               ` (4 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin, Jeff King

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

The new introduced "proc-receive" hook may handle a command for a
pseudo-reference with a zero-old as its old-oid, while the hook may
create or update a reference with different name, different new-oid,
and different old-oid (the reference may exist already with a non-zero
old-oid).  Current "report-status" protocol cannot report the status for
such reference rewrite.

Add new capability "report-status-v2" and new report protocol which is
not backward compatible for report of git-push.

If a user pushes to a pseudo-reference "refs/for/master/topic", and
"receive-pack" creates two new references "refs/changes/23/123/1" and
"refs/changes/24/124/1", for client without the knowledge of
"report-status-v2", "receive-pack" will only send "ok/ng" directives in
the report, such as:

    ok ref/for/master/topic

But for client which has the knowledge of "report-status-v2",
"receive-pack" will use "option" directives to report more attributes
for the reference given by the above "ok/ng" directive.

    ok refs/for/master/topic
    option refname refs/changes/23/123/1
    option new-oid <new-oid>
    ok refs/for/master/topic
    option refname refs/changes/24/124/1
    option new-oid <new-oid>

The client will report two new created references to the end user.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                        |  54 +++++++-
 builtin/send-pack.c                           |  19 +++
 remote.h                                      |   1 +
 send-pack.c                                   | 101 +++++++++++---
 t/t5411-proc-receive-hook.sh                  |   9 ++
 t/t5411/once-0010-report-status-v1.sh         |  90 ++++++++++++
 t/t5411/test-0032-report-with-options.sh      |  14 +-
 ...est-0033-report-with-options--porcelain.sh |  14 +-
 ...t-0036-report-multi-rewrite-for-one-ref.sh |  11 +-
 ...rt-multi-rewrite-for-one-ref--porcelain.sh |  11 +-
 t/t5411/test-0038-report-mixed-refs.sh        |   2 +-
 .../test-0039-report-mixed-refs--porcelain.sh |   2 +-
 transport-helper.c                            | 128 ++++++++++++++----
 transport.c                                   | 116 ++++++++++++----
 14 files changed, 472 insertions(+), 100 deletions(-)
 create mode 100644 t/t5411/once-0010-report-status-v1.sh

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 0390f2dc97..e3dfb17ad4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -57,6 +57,7 @@ static int advertise_push_options;
 static int unpack_limit = 100;
 static off_t max_input_size;
 static int report_status;
+static int report_status_v2;
 static int use_sideband;
 static int use_atomic;
 static int use_push_options;
@@ -240,7 +241,7 @@ static void show_ref(const char *path, const struct object_id *oid)
 		struct strbuf cap = STRBUF_INIT;
 
 		strbuf_addstr(&cap,
-			      "report-status delete-refs side-band-64k quiet");
+			      "report-status report-status-v2 delete-refs side-band-64k quiet");
 		if (advertise_atomic_push)
 			strbuf_addstr(&cap, " atomic");
 		if (prefer_ofs_delta)
@@ -1941,6 +1942,8 @@ static struct command *read_head_info(struct packet_reader *reader,
 			int len = 0;
 			if (parse_feature_request(feature_list, "report-status"))
 				report_status = 1;
+			if (parse_feature_request(feature_list, "report-status-v2"))
+				report_status_v2 = 1;
 			if (parse_feature_request(feature_list, "side-band-64k"))
 				use_sideband = LARGE_PACKET_MAX;
 			if (parse_feature_request(feature_list, "quiet"))
@@ -2259,6 +2262,51 @@ static void report(struct command *commands, const char *unpack_status)
 	strbuf_release(&buf);
 }
 
+static void report_v2(struct command *commands, const char *unpack_status)
+{
+	struct command *cmd;
+	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report *report;
+
+	packet_buf_write(&buf, "unpack %s\n",
+			 unpack_status ? unpack_status : "ok");
+	for (cmd = commands; cmd; cmd = cmd->next) {
+		int count = 0;
+
+		if (cmd->error_string) {
+			packet_buf_write(&buf, "ng %s %s\n",
+					 cmd->ref_name,
+					 cmd->error_string);
+			continue;
+		}
+		packet_buf_write(&buf, "ok %s\n",
+				 cmd->ref_name);
+		for (report = cmd->report; report; report = report->next) {
+			if (count++ > 0)
+				packet_buf_write(&buf, "ok %s\n",
+						 cmd->ref_name);
+			if (report->ref_name)
+				packet_buf_write(&buf, "option refname %s\n",
+						 report->ref_name);
+			if (report->old_oid)
+				packet_buf_write(&buf, "option old-oid %s\n",
+						 oid_to_hex(report->old_oid));
+			if (report->new_oid)
+				packet_buf_write(&buf, "option new-oid %s\n",
+						 oid_to_hex(report->new_oid));
+			if (report->forced_update)
+				packet_buf_write(&buf, "option forced-update\n");
+		}
+	}
+	packet_buf_flush(&buf);
+
+	if (use_sideband)
+		send_sideband(1, 1, buf.buf, buf.len, use_sideband);
+	else
+		write_or_die(1, buf.buf, buf.len);
+	strbuf_release(&buf);
+}
+
 static int delete_only(struct command *commands)
 {
 	struct command *cmd;
@@ -2367,7 +2415,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 				 &push_options);
 		if (pack_lockfile)
 			unlink_or_warn(pack_lockfile);
-		if (report_status)
+		if (report_status_v2)
+			report_v2(commands, unpack_status);
+		else if (report_status)
 			report(commands, unpack_status);
 		run_receive_hook(commands, "post-receive", 1,
 				 &push_options);
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 2b9610f121..7af148d733 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -29,10 +29,12 @@ static struct send_pack_args args;
 static void print_helper_status(struct ref *ref)
 {
 	struct strbuf buf = STRBUF_INIT;
+	struct ref_push_report *report;
 
 	for (; ref; ref = ref->next) {
 		const char *msg = NULL;
 		const char *res;
+		int count = 0;
 
 		switch(ref->status) {
 		case REF_STATUS_NONE:
@@ -94,6 +96,23 @@ static void print_helper_status(struct ref *ref)
 		}
 		strbuf_addch(&buf, '\n');
 
+		if (ref->status == REF_STATUS_OK) {
+			for (report = ref->report; report; report = report->next) {
+				if (count++ > 0)
+					strbuf_addf(&buf, "ok %s\n", ref->name);
+				if (report->ref_name)
+					strbuf_addf(&buf, "option refname %s\n",
+						report->ref_name);
+				if (report->old_oid)
+					strbuf_addf(&buf, "option old-oid %s\n",
+						oid_to_hex(report->old_oid));
+				if (report->new_oid)
+					strbuf_addf(&buf, "option new-oid %s\n",
+						oid_to_hex(report->new_oid));
+				if (report->forced_update)
+					strbuf_addstr(&buf, "option forced-update\n");
+			}
+		}
 		write_or_die(1, buf.buf, buf.len);
 	}
 	strbuf_release(&buf);
diff --git a/remote.h b/remote.h
index 519349333a..eb62a47044 100644
--- a/remote.h
+++ b/remote.h
@@ -148,6 +148,7 @@ struct ref {
 		REF_STATUS_ATOMIC_PUSH_FAILED
 	} status;
 	char *remote_status;
+	struct ref_push_report *report;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
 };
diff --git a/send-pack.c b/send-pack.c
index 632f1580ca..2d2f9997ac 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -154,25 +154,79 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 {
 	struct ref *hint;
 	int ret;
+	struct ref_push_report *report = NULL;
+	int new_report = 0;
+	int once = 0;
 
 	hint = NULL;
 	ret = receive_unpack_status(reader);
 	while (1) {
+		struct object_id old_oid, new_oid;
+		const char *head;
 		const char *refname;
-		char *msg;
+		char *p;
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
-		if (!starts_with(reader->line, "ok ") && !starts_with(reader->line, "ng ")) {
-			error("invalid ref status from remote: %s", reader->line);
+		head = reader->line;
+		p = strchr(head, ' ');
+		if (!p) {
+			error("invalid status line from remote: %s", reader->line);
 			ret = -1;
 			break;
 		}
+		*p++ = '\0';
 
-		refname = reader->line + 3;
-		msg = strchr(refname, ' ');
-		if (msg)
-			*msg++ = '\0';
+		if (!strcmp(head, "option")) {
+			const char *key, *val;
 
+			if (!hint || !(report || new_report)) {
+				if (!once++)
+					error("'option' without a matching 'ok/ng' directive");
+				ret = -1;
+				continue;
+			}
+			if (new_report) {
+				if (!hint->report) {
+					hint->report = xcalloc(1, sizeof(struct ref_push_report));
+					report = hint->report;
+				} else {
+					report = hint->report;
+					while (report->next)
+						report = report->next;
+					report->next = xcalloc(1, sizeof(struct ref_push_report));
+					report = report->next;
+				}
+				new_report = 0;
+			}
+			key = p;
+			p = strchr(key, ' ');
+			if (p)
+				*p++ = '\0';
+			val = p;
+			if (!strcmp(key, "refname"))
+				report->ref_name = xstrdup_or_null(val);
+			else if (!strcmp(key, "old-oid") && val &&
+				 !parse_oid_hex(val, &old_oid, &val))
+				report->old_oid = oiddup(&old_oid);
+			else if (!strcmp(key, "new-oid") && val &&
+				 !parse_oid_hex(val, &new_oid, &val))
+				report->new_oid = oiddup(&new_oid);
+			else if (!strcmp(key, "forced-update"))
+				report->forced_update = 1;
+			continue;
+		}
+
+		report = NULL;
+		new_report = 0;
+		if (strcmp(head, "ok") && strcmp(head, "ng")) {
+			error("invalid ref status from remote: %s", head);
+			ret = -1;
+			break;
+		}
+		refname = p;
+		p = strchr(refname, ' ');
+		if (p)
+			*p++ = '\0';
 		/* first try searching at our hint, falling back to all refs */
 		if (hint)
 			hint = find_ref_by_name(hint, refname);
@@ -180,22 +234,27 @@ static int receive_status(struct packet_reader *reader, struct ref *refs)
 			hint = find_ref_by_name(refs, refname);
 		if (!hint) {
 			warning("remote reported status on unknown ref: %s",
-					refname);
+				refname);
 			continue;
 		}
-		if (hint->status != REF_STATUS_EXPECTING_REPORT) {
+		if (hint->status != REF_STATUS_EXPECTING_REPORT &&
+		    hint->status != REF_STATUS_OK &&
+		    hint->status != REF_STATUS_REMOTE_REJECT) {
 			warning("remote reported status on unexpected ref: %s",
-					refname);
+				refname);
 			continue;
 		}
-
-		if (reader->line[0] == 'o' && reader->line[1] == 'k')
-			hint->status = REF_STATUS_OK;
-		else
+		if (!strcmp(head, "ng")) {
 			hint->status = REF_STATUS_REMOTE_REJECT;
-		hint->remote_status = xstrdup_or_null(msg);
-		/* start our next search from the next ref */
-		hint = hint->next;
+			if (p)
+				hint->remote_status = xstrdup(p);
+			else
+				hint->remote_status = "failed";
+		} else {
+			hint->status = REF_STATUS_OK;
+			hint->remote_status = xstrdup_or_null(p);
+			new_report = 1;
+		}
 	}
 	return ret;
 }
@@ -371,7 +430,9 @@ int send_pack(struct send_pack_args *args,
 	struct packet_reader reader;
 
 	/* Does the other end support the reporting? */
-	if (server_supports("report-status"))
+	if (server_supports("report-status-v2"))
+		status_report = 2;
+	else if (server_supports("report-status"))
 		status_report = 1;
 	if (server_supports("delete-refs"))
 		allow_deleting_refs = 1;
@@ -423,8 +484,10 @@ int send_pack(struct send_pack_args *args,
 
 	use_push_options = push_options_supported && args->push_options;
 
-	if (status_report)
+	if (status_report == 1)
 		strbuf_addstr(&cap_buf, " report-status");
+	else if (status_report == 2)
+		strbuf_addstr(&cap_buf, " report-status-v2");
 	if (use_sideband)
 		strbuf_addstr(&cap_buf, " side-band-64k");
 	if (quiet_supported && (args->quiet || !args->progress))
diff --git a/t/t5411-proc-receive-hook.sh b/t/t5411-proc-receive-hook.sh
index 3a684353a8..746487286f 100755
--- a/t/t5411-proc-receive-hook.sh
+++ b/t/t5411-proc-receive-hook.sh
@@ -78,6 +78,15 @@ run_proc_receive_hook_test() {
 # Initialize the upstream repository and local workbench.
 setup_upstream_and_workbench
 
+# Load test cases that only need to be executed once.
+for t in  "$TEST_DIRECTORY"/t5411/once-*.sh
+do
+	. "$t"
+done
+
+# Initialize the upstream repository and local workbench.
+setup_upstream_and_workbench
+
 # Run test cases for 'proc-receive' hook on local file protocol.
 run_proc_receive_hook_test local
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
new file mode 100644
index 0000000000..bf410dc418
--- /dev/null
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -0,0 +1,90 @@
+test_expect_success "setup proc-receive hook" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/master/topic1" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/23/123/1" \
+		-r "option new-oid $A" \
+		-r "ok refs/for/master/topic2" \
+		-r "option refname refs/for/changes/24/124/2" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update" \
+		-r "ng refs/for/next/topic target branch not exist"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : (B)                   refs/for/master/topic1(A)  foo(A)  refs/for/next/topic(A)  refs/for/master/topic2(A)
+test_expect_success "proc-receive: report status v1" '
+	{
+		if test -z "$GIT_DEFAULT_HASH" || test "$GIT_DEFAULT_HASH" = "sha1"
+		then
+			printf "%s %s refs/heads/master\0report-status\n" \
+				$A $B | packetize
+		else
+			printf "%s %s refs/heads/master\0report-status object-format=$GIT_DEFAULT_HASH\n" \
+				$A $B | packetize
+		fi &&
+		printf "%s %s refs/for/master/topic1\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/heads/foo\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/next/topic\n" \
+			$ZERO_OID $A | packetize &&
+		printf "%s %s refs/for/master/topic2\n" \
+			$ZERO_OID $A | packetize &&
+		printf 0000 &&
+		printf "" | git -C "$upstream" pack-objects --stdout
+	} | git receive-pack "$upstream" --stateless-rpc \
+	>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	# pre-receive hook
+	pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	# proc-receive hook
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic2
+	proc-receive> ok refs/for/master/topic1
+	proc-receive> option fall-through
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/23/123/1
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> ok refs/for/master/topic2
+	proc-receive> option refname refs/for/changes/24/124/2
+	proc-receive> option old-oid <COMMIT-B>
+	proc-receive> option new-oid <COMMIT-A>
+	proc-receive> option forced-update
+	proc-receive> ng refs/for/next/topic target branch not exist
+	000eunpack ok
+	0019ok refs/heads/master
+	001eok refs/for/master/topic1
+	0016ok refs/heads/foo
+	0033ng refs/for/next/topic target branch not exist
+	001eok refs/for/master/topic2
+	0000# post-receive hook
+	post-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic1
+	post-receive< <ZERO-OID> <COMMIT-A> refs/heads/foo
+	post-receive< <ZERO-OID> <COMMIT-A> refs/for/changes/23/123/1
+	post-receive< <COMMIT-B> <COMMIT-A> refs/for/changes/24/124/2
+	EOF
+	test_cmp expect actual &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/for/master/topic1
+	<COMMIT-A> refs/heads/foo
+	<COMMIT-B> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index c559c207fa..b77b78c49f 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -56,7 +56,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -89,7 +89,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -123,7 +123,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/pull/123/head
 	EOF
 	test_cmp expect actual
 '
@@ -155,7 +155,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL)" '
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-B>..<OID-A> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -189,7 +189,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL)
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	EOF
 	test_cmp expect actual
 '
@@ -241,9 +241,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/next/topic
+	 * [new reference] HEAD -> refs/pull/123/head
 	 * [new reference] HEAD -> refs/for/a/b/c/topic
-	 * [new reference] HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
 
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index ea9312234e..1fe352b686 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -57,7 +57,7 @@ test_expect_success "proc-receive: report option refname ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -92,7 +92,7 @@ test_expect_success "proc-receive: report option refname and forced-update ($PRO
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	Done
 	EOF
 	test_cmp expect actual
@@ -127,7 +127,7 @@ test_expect_success "proc-receive: report option refname and old-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/123/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/pull/123/head    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -160,7 +160,7 @@ test_expect_success "proc-receive: report option old-oid ($PROTOCOL/porcelain)"
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-B>..<OID-A>
 	Done
 	EOF
 	test_cmp expect actual
@@ -195,7 +195,7 @@ test_expect_success "proc-receive: report option old-oid and new-oid ($PROTOCOL/
 	remote: # post-receive hook
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual
@@ -249,9 +249,9 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/next/topic    [new reference]
+	*    HEAD:refs/pull/123/head    [new reference]
 	*    HEAD:refs/for/a/b/c/topic    [new reference]
-	*    HEAD:refs/for/master/topic    [new reference]
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 67cc4a8a4c..27d58edd14 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -45,7 +45,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -105,7 +107,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/24/124/1
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
+	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
@@ -152,7 +156,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
-	 * [new reference] HEAD -> refs/for/master/topic
+	 * [new reference] HEAD -> refs/changes/23/123/1
+	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index 1dc8551bc6..77b5b22ed4 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -45,7 +45,9 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/24/124/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/changes/25/125/1    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -106,7 +108,9 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/for/master/topic
 	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/changes/25/125/1
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/24/124/1    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
+	+    HEAD:refs/changes/25/125/1    <OID-B>...<OID-A> (forced update)
 	Done
 	EOF
 	test_cmp expect actual &&
@@ -154,7 +158,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/changes/23/123/1
 	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/changes/24/124/2
 	To <URL/of/upstream.git>
-	*    HEAD:refs/for/master/topic    [new reference]
+	*    HEAD:refs/changes/23/123/1    [new reference]
+	     HEAD:refs/changes/24/124/2    <OID-A>..<OID-B>
 	Done
 	EOF
 	test_cmp expect actual &&
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index bfc8d586d6..a74a2cb449 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 * [new branch] HEAD -> baz
 	 * [new reference] HEAD -> refs/for/next/topic2
 	 * [new branch] HEAD -> foo
-	 * [new reference] HEAD -> refs/for/master/topic
+	 <OID-A>..<OID-B> HEAD -> refs/for/master/topic
 	 ! [remote rejected] HEAD -> refs/for/next/topic1 (fail to call Web API)
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 5d021a4837..e4baa13ea3 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -60,7 +60,7 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	*    HEAD:refs/heads/baz    [new branch]
 	*    HEAD:refs/for/next/topic2    [new reference]
 	*    HEAD:refs/heads/foo    [new branch]
-	*    HEAD:refs/for/master/topic    [new reference]
+	     HEAD:refs/for/master/topic    <OID-A>..<OID-B>
 	!    HEAD:refs/for/next/topic1    [remote rejected] (fail to call Web API)
 	!    HEAD:refs/for/next/topic3    [remote rejected] (proc-receive failed to report status)
 	Done
diff --git a/transport-helper.c b/transport-helper.c
index defafbf4c1..9e55bb89f9 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -723,13 +723,61 @@ static int fetch(struct transport *transport,
 	return -1;
 }
 
+struct push_update_ref_state {
+	struct ref *hint;
+	struct ref_push_report *report;
+	int new_report;
+};
+
 static int push_update_ref_status(struct strbuf *buf,
-				   struct ref **ref,
+				   struct push_update_ref_state *state,
 				   struct ref *remote_refs)
 {
 	char *refname, *msg;
 	int status, forced = 0;
 
+	if (starts_with(buf->buf, "option ")) {
+		struct object_id old_oid, new_oid;
+		const char *key, *val;
+		char *p;
+
+		if (!state->hint || !(state->report || state->new_report))
+			die(_("'option' without a matching 'ok/error' directive"));
+		if (state->new_report) {
+			if (!state->hint->report) {
+				state->hint->report = xcalloc(1, sizeof(struct ref_push_report));
+				state->report = state->hint->report;
+			} else {
+				state->report = state->hint->report;
+				while (state->report->next)
+					state->report = state->report->next;
+				state->report->next = xcalloc(1, sizeof(struct ref_push_report));
+				state->report = state->report->next;
+			}
+			state->new_report = 0;
+		}
+		key = buf->buf + 7;
+		p = strchr(key, ' ');
+		if (p)
+			*p++ = '\0';
+		val = p;
+		if (!strcmp(key, "refname"))
+			state->report->ref_name = xstrdup_or_null(val);
+		else if (!strcmp(key, "old-oid") && val &&
+			 !parse_oid_hex(val, &old_oid, &val))
+			state->report->old_oid = oiddup(&old_oid);
+		else if (!strcmp(key, "new-oid") && val &&
+			 !parse_oid_hex(val, &new_oid, &val))
+			state->report->new_oid = oiddup(&new_oid);
+		else if (!strcmp(key, "forced-update"))
+			state->report->forced_update = 1;
+		/* Not update remote namespace again. */
+		return 1;
+	}
+
+	state->report = NULL;
+	state->new_report = 0;
+
 	if (starts_with(buf->buf, "ok ")) {
 		status = REF_STATUS_OK;
 		refname = buf->buf + 3;
@@ -785,16 +833,16 @@ static int push_update_ref_status(struct strbuf *buf,
 		}
 	}
 
-	if (*ref)
-		*ref = find_ref_by_name(*ref, refname);
-	if (!*ref)
-		*ref = find_ref_by_name(remote_refs, refname);
-	if (!*ref) {
+	if (state->hint)
+		state->hint = find_ref_by_name(state->hint, refname);
+	if (!state->hint)
+		state->hint = find_ref_by_name(remote_refs, refname);
+	if (!state->hint) {
 		warning(_("helper reported unexpected status of %s"), refname);
 		return 1;
 	}
 
-	if ((*ref)->status != REF_STATUS_NONE) {
+	if (state->hint->status != REF_STATUS_NONE) {
 		/*
 		 * Earlier, the ref was marked not to be pushed, so ignore the ref
 		 * status reported by the remote helper if the latter is 'no match'.
@@ -803,9 +851,11 @@ static int push_update_ref_status(struct strbuf *buf,
 			return 1;
 	}
 
-	(*ref)->status = status;
-	(*ref)->forced_update |= forced;
-	(*ref)->remote_status = msg;
+	if (status == REF_STATUS_OK)
+		state->new_report = 1;
+	state->hint->status = status;
+	state->hint->forced_update |= forced;
+	state->hint->remote_status = msg;
 	return !(status == REF_STATUS_OK);
 }
 
@@ -813,37 +863,57 @@ static int push_update_refs_status(struct helper_data *data,
 				    struct ref *remote_refs,
 				    int flags)
 {
+	struct ref *ref;
+	struct ref_push_report *report;
 	struct strbuf buf = STRBUF_INIT;
-	struct ref *ref = remote_refs;
-	int ret = 0;
+	struct push_update_ref_state state = { remote_refs, NULL, 0 };
 
 	for (;;) {
-		char *private;
-
 		if (recvline(data, &buf)) {
-			ret = 1;
-			break;
+			strbuf_release(&buf);
+			return 1;
 		}
-
 		if (!buf.len)
 			break;
+		push_update_ref_status(&buf, &state, remote_refs);
+	}
+	strbuf_release(&buf);
 
-		if (push_update_ref_status(&buf, &ref, remote_refs))
-			continue;
+	if (flags & TRANSPORT_PUSH_DRY_RUN || !data->rs.nr || data->no_private_update)
+		return 0;
 
-		if (flags & TRANSPORT_PUSH_DRY_RUN || !data->rs.nr || data->no_private_update)
-			continue;
+	/* propagate back the update to the remote namespace */
+	for (ref = remote_refs; ref; ref = ref->next) {
+		char *private;
 
-		/* propagate back the update to the remote namespace */
-		private = apply_refspecs(&data->rs, ref->name);
-		if (!private)
+		if (ref->status != REF_STATUS_OK)
 			continue;
-		update_ref("update by helper", private, &ref->new_oid, NULL,
-			   0, 0);
-		free(private);
+
+		if (!ref->report) {
+			private = apply_refspecs(&data->rs, ref->name);
+			if (!private)
+				continue;
+			update_ref("update by helper", private, &(ref->new_oid),
+				   NULL, 0, 0);
+			free(private);
+		} else {
+			for (report = ref->report; report; report = report->next) {
+				private = apply_refspecs(&data->rs,
+							 report->ref_name
+							 ? report->ref_name
+							 : ref->name);
+				if (!private)
+					continue;
+				update_ref("update by helper", private,
+					   report->new_oid
+					   ? report->new_oid
+					   : &(ref->new_oid),
+					   NULL, 0, 0);
+				free(private);
+			}
+		}
 	}
-	strbuf_release(&buf);
-	return ret;
+	return 0;
 }
 
 static void set_common_push_options(struct transport *transport,
diff --git a/transport.c b/transport.c
index 84ec80447b..75e97a0c20 100644
--- a/transport.c
+++ b/transport.c
@@ -461,13 +461,21 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
 
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
+			     struct ref_push_report *report,
 			     int porcelain, int summary_width)
 {
+	const char *to_name;
+
+	if (report && report->ref_name)
+		to_name = report->ref_name;
+	else
+		to_name = to->name;
+
 	if (porcelain) {
 		if (from)
-			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to->name);
+			fprintf(stdout, "%c\t%s:%s\t", flag, from->name, to_name);
 		else
-			fprintf(stdout, "%c\t:%s\t", flag, to->name);
+			fprintf(stdout, "%c\t:%s\t", flag, to_name);
 		if (msg)
 			fprintf(stdout, "%s (%s)\n", summary, msg);
 		else
@@ -481,9 +489,11 @@ static void print_ref_status(char flag, const char *summary,
 		fprintf(stderr, " %s%c %-*s%s ", red, flag, summary_width,
 			summary, reset);
 		if (from)
-			fprintf(stderr, "%s -> %s", prettify_refname(from->name), prettify_refname(to->name));
+			fprintf(stderr, "%s -> %s",
+				prettify_refname(from->name),
+				prettify_refname(to_name));
 		else
-			fputs(prettify_refname(to->name), stderr);
+			fputs(prettify_refname(to_name), stderr);
 		if (msg) {
 			fputs(" (", stderr);
 			fputs(msg, stderr);
@@ -493,27 +503,52 @@ static void print_ref_status(char flag, const char *summary,
 	}
 }
 
-static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_width)
+static void print_ok_ref_status(struct ref *ref,
+				struct ref_push_report *report,
+				int porcelain, int summary_width)
 {
+	struct object_id *old_oid;
+	struct object_id *new_oid;
+	const char *ref_name;
+	int forced_update;
+
+	if (report && report->old_oid)
+		old_oid = report->old_oid;
+	else
+		old_oid = &ref->old_oid;
+	if (report && report->new_oid)
+		new_oid = report->new_oid;
+	else
+		new_oid = &ref->new_oid;
+	if (report && report->forced_update)
+		forced_update = report->forced_update;
+	else
+		forced_update = ref->forced_update;
+	if (report && report->ref_name)
+		ref_name = report->ref_name;
+	else
+		ref_name = ref->name;
+
 	if (ref->deletion)
 		print_ref_status('-', "[deleted]", ref, NULL, NULL,
-				 porcelain, summary_width);
-	else if (is_null_oid(&ref->old_oid))
+				 report, porcelain, summary_width);
+	else if (is_null_oid(old_oid))
 		print_ref_status('*',
-				 (starts_with(ref->name, "refs/tags/")
+				 (starts_with(ref_name, "refs/tags/")
 				  ? "[new tag]"
-				  : (starts_with(ref->name, "refs/heads/")
+				  : (starts_with(ref_name, "refs/heads/")
 				     ? "[new branch]"
 				     : "[new reference]")),
-				 ref, ref->peer_ref, NULL, porcelain, summary_width);
+				 ref, ref->peer_ref, NULL,
+				 report, porcelain, summary_width);
 	else {
 		struct strbuf quickref = STRBUF_INIT;
 		char type;
 		const char *msg;
 
-		strbuf_add_unique_abbrev(&quickref, &ref->old_oid,
+		strbuf_add_unique_abbrev(&quickref, old_oid,
 					 DEFAULT_ABBREV);
-		if (ref->forced_update) {
+		if (forced_update) {
 			strbuf_addstr(&quickref, "...");
 			type = '+';
 			msg = "forced update";
@@ -522,16 +557,17 @@ static void print_ok_ref_status(struct ref *ref, int porcelain, int summary_widt
 			type = ' ';
 			msg = NULL;
 		}
-		strbuf_add_unique_abbrev(&quickref, &ref->new_oid,
+		strbuf_add_unique_abbrev(&quickref, new_oid,
 					 DEFAULT_ABBREV);
 
 		print_ref_status(type, quickref.buf, ref, ref->peer_ref, msg,
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		strbuf_release(&quickref);
 	}
 }
 
-static int print_one_push_status(struct ref *ref, const char *dest, int count,
+static int print_one_push_report(struct ref *ref, const char *dest, int count,
+				 struct ref_push_report *report,
 				 int porcelain, int summary_width)
 {
 	if (!count) {
@@ -543,65 +579,89 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count,
 	switch(ref->status) {
 	case REF_STATUS_NONE:
 		print_ref_status('X', "[no match]", ref, NULL, NULL,
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NODELETE:
 		print_ref_status('!', "[rejected]", ref, NULL,
 				 "remote does not support deleting refs",
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_UPTODATE:
 		print_ref_status('=', "[up to date]", ref,
-				 ref->peer_ref, NULL, porcelain, summary_width);
+				 ref->peer_ref, NULL,
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NONFASTFORWARD:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "non-fast-forward", porcelain, summary_width);
+				 "non-fast-forward",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_ALREADY_EXISTS:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "already exists", porcelain, summary_width);
+				 "already exists",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_FETCH_FIRST:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "fetch first", porcelain, summary_width);
+				 "fetch first",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_NEEDS_FORCE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "needs force", porcelain, summary_width);
+				 "needs force",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_STALE:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "stale info", porcelain, summary_width);
+				 "stale info",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REJECT_SHALLOW:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
 				 "new shallow roots not allowed",
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_REMOTE_REJECT:
 		print_ref_status('!', "[remote rejected]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
-				 ref->remote_status, porcelain, summary_width);
+				 ref->remote_status,
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_EXPECTING_REPORT:
 		print_ref_status('!', "[remote failure]", ref,
 				 ref->deletion ? NULL : ref->peer_ref,
 				 "remote failed to report status",
-				 porcelain, summary_width);
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_ATOMIC_PUSH_FAILED:
 		print_ref_status('!', "[rejected]", ref, ref->peer_ref,
-				 "atomic push failed", porcelain, summary_width);
+				 "atomic push failed",
+				 report, porcelain, summary_width);
 		break;
 	case REF_STATUS_OK:
-		print_ok_ref_status(ref, porcelain, summary_width);
+		print_ok_ref_status(ref, report, porcelain, summary_width);
 		break;
 	}
 
 	return 1;
 }
 
+static int print_one_push_status(struct ref *ref, const char *dest, int count,
+				 int porcelain, int summary_width)
+{
+	struct ref_push_report *report;
+	int n = 0;
+
+	if (!ref->report)
+		return print_one_push_report(ref, dest, count,
+					     NULL, porcelain, summary_width);
+
+	for (report = ref->report; report; report = report->next)
+		print_one_push_report(ref, dest, count + n++,
+				      report, porcelain, summary_width);
+	return n;
+}
+
 static int measure_abbrev(const struct object_id *oid, int sofar)
 {
 	char hex[GIT_MAX_HEXSZ + 1];
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 06/10] doc: add document for capability report-status-v2
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (5 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 05/10] New capability "report-status-v2" for git-push Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
                                                               ` (3 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add ABNF notation for capability 'report-status-v2' which extends
capability 'report-status' by adding additional option lines.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/technical/pack-protocol.txt     | 41 +++++++++++++++++--
 .../technical/protocol-capabilities.txt       | 17 ++++++--
 2 files changed, 52 insertions(+), 6 deletions(-)

diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt
index a4573d12ce..e13a2c064d 100644
--- a/Documentation/technical/pack-protocol.txt
+++ b/Documentation/technical/pack-protocol.txt
@@ -503,8 +503,8 @@ The reference discovery phase is done nearly the same way as it is in the
 fetching protocol. Each reference obj-id and name on the server is sent
 in packet-line format to the client, followed by a flush-pkt.  The only
 real difference is that the capability listing is different - the only
-possible values are 'report-status', 'delete-refs', 'ofs-delta' and
-'push-options'.
+possible values are 'report-status', 'report-status-v2', 'delete-refs',
+'ofs-delta', 'atomic' and 'push-options'.
 
 Reference Update Request and Packfile Transfer
 ----------------------------------------------
@@ -625,7 +625,7 @@ Report Status
 -------------
 
 After receiving the pack data from the sender, the receiver sends a
-report if 'report-status' capability is in effect.
+report if 'report-status' or 'report-status-v2' capability is in effect.
 It is a short listing of what happened in that update.  It will first
 list the status of the packfile unpacking as either 'unpack ok' or
 'unpack [error]'.  Then it will list the status for each of the references
@@ -647,6 +647,41 @@ update was successful, or 'ng [refname] [error]' if the update was not.
   error-msg         = 1*(OCTET) ; where not "ok"
 ----
 
+The 'report-status-v2' capability extends the protocol by adding new option
+lines in order to support reporting of reference rewritten by the
+'proc-receive' hook.  The 'proc-receive' hook may handle a command for a
+pseudo-reference which may create or update one or more references, and each
+reference may have different name, different new-oid, and different old-oid.
+
+----
+  report-status-v2  = unpack-status
+		      1*(command-status-v2)
+		      flush-pkt
+
+  unpack-status     = PKT-LINE("unpack" SP unpack-result)
+  unpack-result     = "ok" / error-msg
+
+  command-status-v2 = command-ok-v2 / command-fail
+  command-ok-v2     = command-ok
+		      *option-line
+
+  command-ok        = PKT-LINE("ok" SP refname)
+  command-fail      = PKT-LINE("ng" SP refname SP error-msg)
+
+  error-msg         = 1*(OCTET) ; where not "ok"
+
+  option-line       = *1(option-refname)
+		      *1(option-old-oid)
+		      *1(option-new-oid)
+		      *1(option-forced-update)
+
+  option-refname    = PKT-LINE("option" SP "refname" SP refname)
+  option-old-oid    = PKT-LINE("option" SP "old-oid" SP obj-id)
+  option-new-oid    = PKT-LINE("option" SP "new-oid" SP obj-id)
+  option-force      = PKT-LINE("option" SP "forced-update")
+
+----
+
 Updates can be unsuccessful for a number of reasons.  The reference can have
 changed since the reference discovery phase was originally sent, meaning
 someone pushed in the meantime.  The reference being pushed could be a
diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index 124d716807..ba869a7d36 100644
--- a/Documentation/technical/protocol-capabilities.txt
+++ b/Documentation/technical/protocol-capabilities.txt
@@ -22,9 +22,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 'atomic', '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', 'report-status-v2', '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
@@ -284,6 +284,17 @@ each reference was updated successfully.  If any of those were not
 successful, it will send back an error message.  See pack-protocol.txt
 for example messages.
 
+report-status-v2
+----------------
+
+Capability 'report-status-v2' extends capability 'report-status' by
+adding new "option" directives in order to support reference rewritten by
+the "proc-receive" hook.  The "proc-receive" hook may handle a command
+for a pseudo-reference which may create or update a reference with
+different name, new-oid, and old-oid.  While the capability
+'report-status' cannot report for such case.  See pack-protocol.txt
+for details.
+
 delete-refs
 -----------
 
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 07/10] receive-pack: new config receive.procReceiveRefs
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (6 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 06/10] doc: add document for capability report-status-v2 Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 08/10] t5411: test updates of remote-tracking branches Jiang Xin
                                                               ` (2 subsequent siblings)
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add a new multi-valued config variable "receive.procReceiveRefs"
for `receive-pack` command, like the follows:

    git config --system --add receive.procReceiveRefs refs/for
    git config --system --add receive.procReceiveRefs refs/drafts

If the specific prefix strings given by the config variables match the
reference names of the commands which are sent from git client to
`receive-pack`, these commands will be executed by an external hook
(named "proc-receive"), instead of the internal `execute_commands`
function.

For example, if it is set to "refs/for", pushing to a reference such as
"refs/for/master" will not create or update reference "refs/for/master",
but may create or update a pull request directly by running the hook
"proc-receive".

Optional modifiers can be provided in the beginning of the value to
filter commands for specific actions: create (a), modify (m),
delete (d). A `!` can be included in the modifiers to negate the
reference prefix entry. E.g.:

    git config --system --add receive.procReceiveRefs ad:refs/heads
    git config --system --add receive.procReceiveRefs !:refs/heads

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/config/receive.txt              |  22 +++
 builtin/receive-pack.c                        | 106 +++++++++++++-
 t/t5411/once-0010-report-status-v1.sh         |   4 +
 t/t5411/test-0010-proc-receive-settings.sh    |   7 +
 t/t5411/test-0040-process-all-refs.sh         | 113 +++++++++++++++
 .../test-0041-process-all-refs--porcelain.sh  | 114 +++++++++++++++
 ...t-0050-proc-receive-refs-with-modifiers.sh | 135 ++++++++++++++++++
 7 files changed, 494 insertions(+), 7 deletions(-)
 create mode 100644 t/t5411/test-0010-proc-receive-settings.sh
 create mode 100644 t/t5411/test-0040-process-all-refs.sh
 create mode 100644 t/t5411/test-0041-process-all-refs--porcelain.sh
 create mode 100644 t/t5411/test-0050-proc-receive-refs-with-modifiers.sh

diff --git a/Documentation/config/receive.txt b/Documentation/config/receive.txt
index 65f78aac37..85d5b5a3d2 100644
--- a/Documentation/config/receive.txt
+++ b/Documentation/config/receive.txt
@@ -114,6 +114,28 @@ receive.hideRefs::
 	An attempt to update or delete a hidden ref by `git push` is
 	rejected.
 
+receive.procReceiveRefs::
+	This is a multi-valued variable that defines reference prefixes
+	to match the commands in `receive-pack`.  Commands matching the
+	prefixes will be executed by an external hook "proc-receive",
+	instead of the internal `execute_commands` function.  If this
+	variable is not defined, the "proc-receive" hook will never be
+	used, and all commands will be executed by the internal
+	`execute_commands` function.
++
+For example, if this variable is set to "refs/for", pushing to reference
+such as "refs/for/master" will not create or update a reference named
+"refs/for/master", but may create or update a pull request directly by
+running the hook "proc-receive".
++
+Optional modifiers can be provided in the beginning of the value to filter
+commands for specific actions: create (a), modify (m), delete (d).
+A `!` can be included in the modifiers to negate the reference prefix entry.
+E.g.:
++
+	git config --system --add receive.procReceiveRefs ad:refs/heads
+	git config --system --add receive.procReceiveRefs !:refs/heads
+
 receive.updateServerInfo::
 	If set to true, git-receive-pack will run git-update-server-info
 	after receiving data from git-push and updating refs.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e3dfb17ad4..bb9909c52e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -98,6 +98,17 @@ static int keepalive_in_sec = 5;
 
 static struct tmp_objdir *tmp_objdir;
 
+static struct proc_receive_ref {
+	unsigned int want_add:1,
+		     want_delete:1,
+		     want_modify:1,
+		     negative_ref:1;
+	char *ref_prefix;
+	struct proc_receive_ref *next;
+} *proc_receive_ref;
+
+static void proc_receive_ref_append(const char *prefix);
+
 static enum deny_action parse_deny_action(const char *var, const char *value)
 {
 	if (value) {
@@ -230,6 +241,13 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (strcmp(var, "receive.procreceiverefs") == 0) {
+		if (!value)
+			return config_error_nonbool(var);
+		proc_receive_ref_append(value);
+		return 0;
+	}
+
 	return git_default_config(var, value, cb);
 }
 
@@ -326,6 +344,79 @@ struct command {
 	char ref_name[FLEX_ARRAY]; /* more */
 };
 
+static void proc_receive_ref_append(const char *prefix)
+{
+	struct proc_receive_ref *ref_pattern;
+	char *p;
+	int len;
+
+	ref_pattern = xcalloc(1, sizeof(struct proc_receive_ref));
+	p = strchr(prefix, ':');
+	if (p) {
+		while (prefix < p) {
+			if (*prefix == 'a')
+				ref_pattern->want_add = 1;
+			else if (*prefix == 'd')
+				ref_pattern->want_delete = 1;
+			else if (*prefix == 'm')
+				ref_pattern->want_modify = 1;
+			else if (*prefix == '!')
+				ref_pattern->negative_ref = 1;
+			prefix++;
+		}
+		prefix++;
+	} else {
+		ref_pattern->want_add = 1;
+		ref_pattern->want_delete = 1;
+		ref_pattern->want_modify = 1;
+	}
+	len = strlen(prefix);
+	while (len && prefix[len - 1] == '/')
+		len--;
+	ref_pattern->ref_prefix = xmemdupz(prefix, len);
+	if (!proc_receive_ref) {
+		proc_receive_ref = ref_pattern;
+	} else {
+		struct proc_receive_ref *end;
+
+		end = proc_receive_ref;
+		while (end->next)
+			end = end->next;
+		end->next = ref_pattern;
+	}
+}
+
+static int proc_receive_ref_matches(struct command *cmd)
+{
+	struct proc_receive_ref *p;
+
+	if (!proc_receive_ref)
+		return 0;
+
+	for (p = proc_receive_ref; p; p = p->next) {
+		const char *match = p->ref_prefix;
+		const char *remains;
+
+		if (!p->want_add && is_null_oid(&cmd->old_oid))
+			continue;
+		else if (!p->want_delete && is_null_oid(&cmd->new_oid))
+			continue;
+		else if (!p->want_modify &&
+			 !is_null_oid(&cmd->old_oid) &&
+			 !is_null_oid(&cmd->new_oid))
+			continue;
+
+		if (skip_prefix(cmd->ref_name, match, &remains) &&
+		    (!*remains || *remains == '/')) {
+			if (!p->negative_ref)
+				return 1;
+		} else if (p->negative_ref) {
+			return 1;
+		}
+	}
+	return 0;
+}
+
 static void rp_error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 static void rp_warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 
@@ -1814,14 +1905,15 @@ static void execute_commands(struct command *commands,
 	 * Try to find commands that have special prefix in their reference names,
 	 * and mark them to run an external "proc-receive" hook later.
 	 */
-	for (cmd = commands; cmd; cmd = cmd->next) {
-		if (!should_process_cmd(cmd))
-			continue;
+	if (proc_receive_ref) {
+		for (cmd = commands; cmd; cmd = cmd->next) {
+			if (!should_process_cmd(cmd))
+				continue;
 
-		/* TODO: replace the fixed prefix by looking up git config variables. */
-		if (!strncmp(cmd->ref_name, "refs/for/", 9)) {
-			cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
-			run_proc_receive = 1;
+			if (proc_receive_ref_matches(cmd)) {
+				cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED;
+				run_proc_receive = 1;
+			}
 		}
 	}
 
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index bf410dc418..dc2cf4a522 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -1,3 +1,7 @@
+test_expect_success "setup receive.procReceiveRefs" '
+	git -C "$upstream" config --add receive.procReceiveRefs refs/for
+'
+
 test_expect_success "setup proc-receive hook" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
diff --git a/t/t5411/test-0010-proc-receive-settings.sh b/t/t5411/test-0010-proc-receive-settings.sh
new file mode 100644
index 0000000000..a36809927b
--- /dev/null
+++ b/t/t5411/test-0010-proc-receive-settings.sh
@@ -0,0 +1,7 @@
+test_expect_success "add two receive.procReceiveRefs settings" '
+	(
+		cd "$upstream" &&
+		git config --add receive.procReceiveRefs refs/for &&
+		git config --add receive.procReceiveRefs refs/review/
+	)
+'
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
new file mode 100644
index 0000000000..b07c999f53
--- /dev/null
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -0,0 +1,113 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
+	git -C workbench push -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> bar
+	 - [deleted] foo
+	 + <OID-B>...<OID-A> HEAD -> master (forced update)
+	 <OID-A>..<OID-B> HEAD -> refs/pull/123/head
+	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
new file mode 100644
index 0000000000..0dd9824616
--- /dev/null
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -0,0 +1,114 @@
+test_expect_success "config receive.procReceiveRefs = refs ($PROTOCOL/porcelain)" '
+	git -C "$upstream" config --unset-all receive.procReceiveRefs &&
+	git -C "$upstream" config --add receive.procReceiveRefs refs
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream branches ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref refs/heads/master $B &&
+		git update-ref refs/heads/foo $A &&
+		git update-ref refs/heads/bar $A &&
+		git update-ref refs/heads/baz $A
+	)
+
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option fall-through" \
+		-r "ok refs/heads/foo" \
+		-r "option fall-through" \
+		-r "ok refs/heads/bar" \
+		-r "option fall-through" \
+		-r "ok refs/for/master/topic" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/for/next/topic" \
+		-r "option refname refs/pull/124/head" \
+		-r "option old-oid $B" \
+		-r "option new-oid $A" \
+		-r "option forced-update"
+	EOF
+'
+
+# Refs of upstream : master(B)             foo(A)  bar(A))  baz(A)
+# Refs of workbench: master(A)  tags/v123
+# git push -f      : master(A)             (NULL)  (B)              refs/for/master/topic(A)  refs/for/next/topic(A)
+test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
+	git -C workbench push --porcelain -f origin \
+		HEAD:refs/heads/master \
+		:refs/heads/foo \
+		$B:refs/heads/bar \
+		HEAD:refs/for/master/topic \
+		HEAD:refs/for/next/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: pre-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: proc-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/next/topic
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/foo
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/heads/bar
+	remote: proc-receive> option fall-through
+	remote: proc-receive> ok refs/for/master/topic
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/for/next/topic
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option old-oid <COMMIT-B>
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: proc-receive> option forced-update
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/bar
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/heads/foo
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/heads/master
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <COMMIT-B> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	     <COMMIT-B>:refs/heads/bar    <OID-A>..<OID-B>
+	-    :refs/heads/foo    [deleted]
+	+    HEAD:refs/heads/master    <OID-B>...<OID-A> (forced update)
+	     HEAD:refs/pull/123/head    <OID-A>..<OID-B>
+	+    HEAD:refs/pull/124/head    <OID-B>...<OID-A> (forced update)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-B> refs/heads/bar
+	<COMMIT-A> refs/heads/baz
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)             bar(A)  baz(B)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	(
+		cd "$upstream" &&
+		git update-ref -d refs/heads/bar &&
+		git update-ref -d refs/heads/baz
+	)
+'
diff --git a/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
new file mode 100644
index 0000000000..c22849cbe2
--- /dev/null
+++ b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
@@ -0,0 +1,135 @@
+test_expect_success "config receive.procReceiveRefs with modifiers ($PROTOCOL)" '
+	(
+		cd "$upstream" &&
+		git config --unset-all receive.procReceiveRefs &&
+		git config --add receive.procReceiveRefs m:refs/heads/master &&
+		git config --add receive.procReceiveRefs ad:refs/heads &&
+		git config --add receive.procReceiveRefs "a!:refs/heads"
+	)
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $B" \
+		-r "ok refs/tags/v123 " \
+		-r "option refname refs/pull/124/head"
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         : master(B)  tags/v123
+test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
+	git -C workbench push origin \
+		$B:refs/heads/master \
+		v123 >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: pre-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <COMMIT-B> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <TAG-v123> refs/tags/v123
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <COMMIT-B>
+	remote: proc-receive> ok refs/tags/v123
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/pull/123/head
+	remote: post-receive< <ZERO-OID> <TAG-v123> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 <OID-A>..<OID-B> <COMMIT-B> -> refs/pull/123/head
+	 * [new reference] v123 -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
+	git -C "$upstream" update-ref refs/heads/topic $A &&
+	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-A> refs/heads/topic
+	<TAG-v123> refs/tags/v123
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/heads/master" \
+		-r "option refname refs/pull/123/head" \
+		-r "option old-oid $A" \
+		-r "option new-oid $ZERO_OID" \
+		-r "ok refs/heads/next" \
+		-r "option refname refs/pull/124/head" \
+		-r "option new-oid $A"
+	EOF
+'
+
+# Refs of upstream : master(A)  topic(A)  tags/v123
+# Refs of workbench: master(A)            tags/v123
+# git push         : NULL       topic(B)  NULL       next(A)
+test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOCOL)" '
+	git -C workbench push origin \
+		:refs/heads/master \
+		$B:refs/heads/topic \
+		$A:refs/heads/next \
+		:refs/tags/v123 >out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
+	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/topic
+	remote: pre-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: # proc-receive hook
+	remote: proc-receive< <COMMIT-A> <ZERO-OID> refs/heads/master
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: proc-receive> ok refs/heads/master
+	remote: proc-receive> option refname refs/pull/123/head
+	remote: proc-receive> option old-oid <COMMIT-A>
+	remote: proc-receive> option new-oid <ZERO-OID>
+	remote: proc-receive> ok refs/heads/next
+	remote: proc-receive> option refname refs/pull/124/head
+	remote: proc-receive> option new-oid <COMMIT-A>
+	remote: # post-receive hook
+	remote: post-receive< <COMMIT-A> <ZERO-OID> refs/pull/123/head
+	remote: post-receive< <COMMIT-A> <COMMIT-B> refs/heads/topic
+	remote: post-receive< <TAG-v123> <ZERO-OID> refs/tags/v123
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/pull/124/head
+	To <URL/of/upstream.git>
+	 - [deleted] refs/pull/123/head
+	 <OID-A>..<OID-B> <COMMIT-B> -> topic
+	 - [deleted] v123
+	 * [new reference] <COMMIT-A> -> refs/pull/124/head
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	<COMMIT-B> refs/heads/topic
+	EOF
+	test_cmp expect actual
+'
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 08/10] t5411: test updates of remote-tracking branches
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (7 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 09/10] transport: parse report options for tracking refs Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 10/10] doc: add documentation for the proc-receive hook Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

In order to test update of remote-tracking branches for special refs,
add new "remote.origin.fetch" settings and test cases.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 48 +++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 27d58edd14..8eec3a1ed8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -1,3 +1,18 @@
+test_expect_success "setup git config for remote-tracking of special refs" '
+	(
+		cd workbench &&
+		if ! git config --get-all remote.origin.fetch | grep refs/for/
+		then
+			git config --add remote.origin.fetch \
+				"+refs/for/*:refs/t/for/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/pull/*:refs/t/pull/*" &&
+			git config --add remote.origin.fetch \
+				"+refs/changes/*:refs/t/changes/*"
+		fi
+	)
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 1st rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -58,6 +73,17 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -120,6 +146,17 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	test_cmp expect actual
 '
 
+test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
+
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -167,3 +204,14 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	EOF
 	test_cmp expect actual
 '
+
+test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
+	git -C workbench show-ref |
+		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/t/for/master/topic
+	EOF
+	test_cmp expect actual &&
+	git -C workbench update-ref -d refs/t/for/master/topic
+'
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 09/10] transport: parse report options for tracking refs
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (8 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 08/10] t5411: test updates of remote-tracking branches Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  2020-08-27 15:45                                             ` [PATCH v19 10/10] doc: add documentation for the proc-receive hook Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

When pushing a pseudo reference (such as "refs/for/master/topic"), may
create or update one or more references.  The real names of the
references will be stored in the report options.  Parse report options
to create or update remote-tracking branches properly.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 22 ++++++++---
 transport.c                                   | 37 +++++++++++++++----
 2 files changed, 45 insertions(+), 14 deletions(-)

diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index 8eec3a1ed8..73283d81e8 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -78,10 +78,14 @@ test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-B> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL)" '
@@ -151,10 +155,14 @@ test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/24/124/1
+	<COMMIT-A> refs/t/changes/25/125/1
+	<COMMIT-B> refs/t/for/master/topic
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/for/master/topic &&
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/1
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL)" '
@@ -210,8 +218,10 @@ test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
 		grep -v -e refs/remotes -e refs/heads -e refs/tags >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-	<COMMIT-A> refs/t/for/master/topic
+	<COMMIT-A> refs/t/changes/23/123/1
+	<COMMIT-B> refs/t/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
-	git -C workbench update-ref -d refs/t/for/master/topic
+	git -C workbench update-ref -d refs/t/changes/24/124/1 &&
+	git -C workbench update-ref -d refs/t/changes/25/125/2
 '
diff --git a/transport.c b/transport.c
index 75e97a0c20..213ec69d7a 100644
--- a/transport.c
+++ b/transport.c
@@ -436,29 +436,50 @@ int transport_refs_pushed(struct ref *ref)
 	return 0;
 }
 
-void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+static void update_one_tracking_ref(struct remote *remote, char *refname,
+				    struct object_id *new_oid, int deletion,
+				    int verbose)
 {
 	struct refspec_item rs;
 
-	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
-		return;
-
 	memset(&rs, 0, sizeof(rs));
-	rs.src = ref->name;
+	rs.src = refname;
 	rs.dst = NULL;
 
 	if (!remote_find_tracking(remote, &rs)) {
 		if (verbose)
 			fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst);
-		if (ref->deletion) {
+		if (deletion)
 			delete_ref(NULL, rs.dst, NULL, 0);
-		} else
-			update_ref("update by push", rs.dst, &ref->new_oid,
+		else
+			update_ref("update by push", rs.dst, new_oid,
 				   NULL, 0, 0);
 		free(rs.dst);
 	}
 }
 
+void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
+{
+	char *refname;
+	struct object_id *new_oid;
+	struct ref_push_report *report;
+
+	if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
+		return;
+
+	report = ref->report;
+	if (!report)
+		update_one_tracking_ref(remote, ref->name, &ref->new_oid,
+					ref->deletion, verbose);
+	else
+		for (; report; report = report->next) {
+			refname = report->ref_name ? (char *)report->ref_name : ref->name;
+			new_oid = report->new_oid ? report->new_oid : &ref->new_oid;
+			update_one_tracking_ref(remote, refname, new_oid,
+						is_null_oid(new_oid), verbose);
+		}
+}
+
 static void print_ref_status(char flag, const char *summary,
 			     struct ref *to, struct ref *from, const char *msg,
 			     struct ref_push_report *report,
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v19 10/10] doc: add documentation for the proc-receive hook
  2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
                                                               ` (9 preceding siblings ...)
  2020-08-27 15:45                                             ` [PATCH v19 09/10] transport: parse report options for tracking refs Jiang Xin
@ 2020-08-27 15:45                                             ` Jiang Xin
  10 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-08-27 15:45 UTC (permalink / raw)
  To: Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

"git receive-pack" that accepts requests by "git push" learned to
outsource some of the ref updates to the new "proc-receive" hook.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 Documentation/githooks.txt | 62 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 31b601e4bc..2899e80445 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -333,6 +333,68 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option fall-through)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status can be given in option directives.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option refname <refname>)
+    H: PKT-LINE(option old-oid <old-oid>)
+    H: PKT-LINE(option new-oid <new-oid>)
+    H: PKT-LINE(option forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook will use
+"option" directives to report extended attributes for the reference given
+by the leading "ok" directive.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
-- 
2.26.2.543.g44b58e439b


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 00/10] New proc-receive hook for centralized workflow
  2020-08-27 15:45                                             ` [PATCH v19 " Jiang Xin
@ 2020-08-27 19:57                                               ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-08-27 19:57 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
>
> ## Changes since v18
>
> 1. This series is based on "next" branch, in order to resolve a conflict
>    with commit 95e7c38539 (refspec: make sure stack refspec_item
>    variables are zeroed, 2020-08-14).  See patch 9/10.

We cannot ever merge such a topic that depends on 'next' down to
'master' without dragging all the other topics that present in
'next', plus the merge commits that bring these topics to 'next'.

In this particular case, I think the conflict resolution is trivial
and more importantly, it is not a *new* conflict v19 introduces but
did not exist with v18.  IOW, what is in 'seen' has already resolved
the same conflict between 95e7c38539 and this topic.

I've applied these directly on 'next' (call it c0), rebased the
result on the same base as v18 is queued on, and then merged the
rebased v19 (call it c1) with 'next' to make sure that the result
(call it c2) matches, i.e. tree of c0 and tree of c2 are identical.

I'll use c1 to replace what is queued in 'seen'.

A workable alternative would be to base these on top of the topic
that contains 95e7c38539 (i.e. jk/refspecs-cleanup).  Then, this
topic is still taken hostage by that 'cleanup' topic, but at least
as long as that topic graduates, this topic can graduate to 'master'
without having to drag any other cruft with it.

Thanks.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2020-08-27 15:45                                             ` [PATCH v19 03/10] receive-pack: add new " Jiang Xin
@ 2020-11-04 22:15                                               ` Johannes Schindelin
  2020-11-04 22:58                                                 ` Johannes Schindelin
  2020-11-05 14:54                                                 ` Jiang Xin
  2021-01-17 22:21                                               ` SZEDER Gábor
  1 sibling, 2 replies; 266+ messages in thread
From: Johannes Schindelin @ 2020-11-04 22:15 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Git List, Jiang Xin

[-- Attachment #1: Type: text/plain, Size: 3860 bytes --]

Hi,

On Thu, 27 Aug 2020, Jiang Xin wrote:

> +test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
> +	write_script "$upstream/hooks/proc-receive" <<-EOF
> +	printf >&2 "# proc-receive hook\n"
> +	test-tool proc-receive -v --die-readline
> +	EOF
> +'
> +
> +# Refs of upstream : master(A)
> +# Refs of workbench: master(A)  tags/v123
> +# git push         :                       refs/for/master/topic(A)
> +test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
> +	test_must_fail git -C workbench push origin \
> +		HEAD:refs/for/master/topic \
> +		>out 2>&1 &&
> +	make_user_friendly_and_stable_output <out >actual &&
> +
> +	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
> +
> +	git -C "$upstream" show-ref >out &&
> +	make_user_friendly_and_stable_output <out >actual &&
> +	cat >expect <<-EOF &&
> +	<COMMIT-A> refs/heads/master
> +	EOF
> +	test_cmp expect actual
> +'

While investigating a flaky hang in t5411 in the osx-clang job of our
CI/PR builds, I ran into this issue (with --stress):

-- snip --
[...]
+++ test_must_fail git -C workbench push origin HEAD:refs/for/main/topic
+++ case "$1" in
+++ _test_ok=
+++ test_must_fail_acceptable git -C workbench push origin HEAD:refs/for/main/topic
+++ test git = env
+++ case "$1" in
+++ return 0
+++ git -C workbench push origin HEAD:refs/for/main/topic
+++ exit_code=128
+++ test 128 -eq 0
+++ test_match_signal 13 128
+++ test 128 = 141
+++ test 128 = 269
+++ return 1
+++ test 128 -gt 129
+++ test 128 -eq 127
+++ test 128 -eq 126
+++ return 0
+++ make_user_friendly_and_stable_output
++++ echo 4844d8acf2fc62b151a29cfb3b916b6778476b9e
++++ cut -c1-7
++++ echo c208c3bb8a1aeea6d4e40027a17251ccd59a2b1b
++++ cut -c1-7
+++ sed -e 's/  *$//' -e 's/   */ /g' -e 's/'\''/"/g' -e 's/    /    /g' -e 's/4844d8acf2fc62b151a29cfb3b916b6778476b9e/<COMMIT-A>/g' -e 's/c208c3bb8a1aeea6d4e40027a17251ccd59a2b1b/<COMMIT-B>/g' -e 's/f7cc02ec1b3393b9a9638a0e02a7039c234cffa4/<TAG-v123>/g' -e 's/0000000000000000000000000000000000000000/<ZERO-OID>/g' -e 's/4844d8a[0-9a-f]*/<OID-A>/g' -e 's/c208c3b[0-9a-f]*/<OID-B>/g' -e 's#To \.\./upstream.git#To <URL/of/upstream.git>#' -e '/^error: / d'
+++ grep 'remote: fatal: protocol error: expected "old new ref", got "<ZERO-OID> <COMMIT-A> refs/for/main/topic"' actual
error: last command exited with $?=1
not ok 34 - proc-receive: bad protocol (hook --die-readline, builtin protocol)
#
#               test_must_fail git -C workbench push origin \
#                       HEAD:refs/for/main/topic \
#                       >out 2>&1 &&
#               make_user_friendly_and_stable_output <out >actual &&
#
#               grep "remote: fatal: protocol error: expected \"old new
#               ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\""
#               actual &&
#
#               git -C "$upstream" show-ref >out &&
#               make_user_friendly_and_stable_output <out >actual &&
#               cat >expect <<-EOF &&
#               <COMMIT-A> refs/heads/main
#               EOF
#               test_cmp expect actual
#
-- snap --

The output of `actual` reads like this:

-- snip --
remote: # pre-receive hook
remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
remote: # proc-receive hook
fatal: unable to write flush packet: Broken pipe
send-pack: unexpected disconnect while reading sideband packet
fatal: the remote end hung up unexpectedly
-- snap --

Applying Gábor's patch as obtained from
https://lore.kernel.org/git/20190830121005.GI8571@szeder.dev/ seemed to
help this issue at first, but then turned out not to prevent the same
issue from happening again.

Any ideas?

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2020-11-04 22:15                                               ` Johannes Schindelin
@ 2020-11-04 22:58                                                 ` Johannes Schindelin
  2020-11-05 14:54                                                 ` Jiang Xin
  1 sibling, 0 replies; 266+ messages in thread
From: Johannes Schindelin @ 2020-11-04 22:58 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Git List, Jiang Xin

[-- Attachment #1: Type: text/plain, Size: 5710 bytes --]

Me again,

On Wed, 4 Nov 2020, Johannes Schindelin wrote:

> On Thu, 27 Aug 2020, Jiang Xin wrote:
>
> > +test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
> > +	write_script "$upstream/hooks/proc-receive" <<-EOF
> > +	printf >&2 "# proc-receive hook\n"
> > +	test-tool proc-receive -v --die-readline
> > +	EOF
> > +'
> > +
> > +# Refs of upstream : master(A)
> > +# Refs of workbench: master(A)  tags/v123
> > +# git push         :                       refs/for/master/topic(A)
> > +test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
> > +	test_must_fail git -C workbench push origin \
> > +		HEAD:refs/for/master/topic \
> > +		>out 2>&1 &&
> > +	make_user_friendly_and_stable_output <out >actual &&
> > +
> > +	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
> > +
> > +	git -C "$upstream" show-ref >out &&
> > +	make_user_friendly_and_stable_output <out >actual &&
> > +	cat >expect <<-EOF &&
> > +	<COMMIT-A> refs/heads/master
> > +	EOF
> > +	test_cmp expect actual
> > +'
>
> While investigating a flaky hang in t5411 in the osx-clang job of our
> CI/PR builds, I ran into this issue (with --stress):
>
> -- snip --
> [...]
> +++ test_must_fail git -C workbench push origin HEAD:refs/for/main/topic
> +++ case "$1" in
> +++ _test_ok=
> +++ test_must_fail_acceptable git -C workbench push origin HEAD:refs/for/main/topic
> +++ test git = env
> +++ case "$1" in
> +++ return 0
> +++ git -C workbench push origin HEAD:refs/for/main/topic
> +++ exit_code=128
> +++ test 128 -eq 0
> +++ test_match_signal 13 128
> +++ test 128 = 141
> +++ test 128 = 269
> +++ return 1
> +++ test 128 -gt 129
> +++ test 128 -eq 127
> +++ test 128 -eq 126
> +++ return 0
> +++ make_user_friendly_and_stable_output
> ++++ echo 4844d8acf2fc62b151a29cfb3b916b6778476b9e
> ++++ cut -c1-7
> ++++ echo c208c3bb8a1aeea6d4e40027a17251ccd59a2b1b
> ++++ cut -c1-7
> +++ sed -e 's/  *$//' -e 's/   */ /g' -e 's/'\''/"/g' -e 's/    /    /g' -e 's/4844d8acf2fc62b151a29cfb3b916b6778476b9e/<COMMIT-A>/g' -e 's/c208c3bb8a1aeea6d4e40027a17251ccd59a2b1b/<COMMIT-B>/g' -e 's/f7cc02ec1b3393b9a9638a0e02a7039c234cffa4/<TAG-v123>/g' -e 's/0000000000000000000000000000000000000000/<ZERO-OID>/g' -e 's/4844d8a[0-9a-f]*/<OID-A>/g' -e 's/c208c3b[0-9a-f]*/<OID-B>/g' -e 's#To \.\./upstream.git#To <URL/of/upstream.git>#' -e '/^error: / d'
> +++ grep 'remote: fatal: protocol error: expected "old new ref", got "<ZERO-OID> <COMMIT-A> refs/for/main/topic"' actual
> error: last command exited with $?=1
> not ok 34 - proc-receive: bad protocol (hook --die-readline, builtin protocol)
> #
> #               test_must_fail git -C workbench push origin \
> #                       HEAD:refs/for/main/topic \
> #                       >out 2>&1 &&
> #               make_user_friendly_and_stable_output <out >actual &&
> #
> #               grep "remote: fatal: protocol error: expected \"old new
> #               ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\""
> #               actual &&
> #
> #               git -C "$upstream" show-ref >out &&
> #               make_user_friendly_and_stable_output <out >actual &&
> #               cat >expect <<-EOF &&
> #               <COMMIT-A> refs/heads/main
> #               EOF
> #               test_cmp expect actual
> #
> -- snap --
>
> The output of `actual` reads like this:
>
> -- snip --
> remote: # pre-receive hook
> remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
> remote: # proc-receive hook
> fatal: unable to write flush packet: Broken pipe
> send-pack: unexpected disconnect while reading sideband packet
> fatal: the remote end hung up unexpectedly
> -- snap --
>
> Applying Gábor's patch as obtained from
> https://lore.kernel.org/git/20190830121005.GI8571@szeder.dev/ seemed to
> help this issue at first, but then turned out not to prevent the same
> issue from happening again.
>
> Any ideas?

More puzzles. I was finally able to reproduce the hang in a `--stress`
run. It basically happens in t5411.186, which is stalled at this:

-- snip --
[...]
expecting success of 5411.186 'proc-receive: bad protocol (hook --die-readline, HTTP protocol)':
        test_must_fail git -C workbench push origin \
                HEAD:refs/for/main/topic \
                >out 2>&1 &&
        make_user_friendly_and_stable_output <out >actual &&

        grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\"" actual &&

        git -C "$upstream" show-ref >out &&
        make_user_friendly_and_stable_output <out >actual &&
        cat >expect <<-EOF &&
        <COMMIT-A> refs/heads/main
        EOF
        test_cmp expect actual

+++ test_must_fail git -C workbench push origin HEAD:refs/for/main/topic
+++ case "$1" in
+++ _test_ok=
+++ test_must_fail_acceptable git -C workbench push origin HEAD:refs/for/main/topic
+++ test git = env
+++ case "$1" in
+++ return 0
+++ git -C workbench push origin HEAD:refs/for/main/topic
-- snap --

That `push` never goes anywhere. And I found something of a clue at
`trash directory.t5411-proc-receive-hook.stress-*/httpd/error.log`:

-- snip --
[...]
[Wed Nov 04 22:28:20.838388 2020] [cgi:error] [pid 60577] [client 127.0.0.1:52794] AH01215: fatal: unable to write flush packet: Broken pipe: /Users/runner/work/git/git/git-http-backend
-- snap --

This is once again running _with_ Gábor's patch mentioned in the quoted
part above.

Could you please look into this?

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2020-11-04 22:15                                               ` Johannes Schindelin
  2020-11-04 22:58                                                 ` Johannes Schindelin
@ 2020-11-05 14:54                                                 ` Jiang Xin
  2020-11-05 15:23                                                   ` [RFC PATCH] t5411: fix broken pipe write error on proc-receive Jiang Xin
  2020-11-05 18:39                                                   ` [PATCH v19 03/10] receive-pack: add new proc-receive hook Junio C Hamano
  1 sibling, 2 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-05 14:54 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Junio C Hamano, Git List, Jiang Xin

Johannes Schindelin <Johannes.Schindelin@gmx.de> 于2020年11月5日周四 上午6:15写道:
> not ok 34 - proc-receive: bad protocol (hook --die-readline, builtin protocol)
> #
> #               test_must_fail git -C workbench push origin \
> #                       HEAD:refs/for/main/topic \
> #                       >out 2>&1 &&
> #               make_user_friendly_and_stable_output <out >actual &&
> #
> #               grep "remote: fatal: protocol error: expected \"old new
> #               ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\""
> #               actual &&
> #
> #               git -C "$upstream" show-ref >out &&
> #               make_user_friendly_and_stable_output <out >actual &&
> #               cat >expect <<-EOF &&
> #               <COMMIT-A> refs/heads/main
> #               EOF
> #               test_cmp expect actual
> #
> -- snap --
>
> The output of `actual` reads like this:
>
> -- snip --
> remote: # pre-receive hook
> remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
> remote: # proc-receive hook
> fatal: unable to write flush packet: Broken pipe
> send-pack: unexpected disconnect while reading sideband packet
> fatal: the remote end hung up unexpectedly
> -- snap --

In this test case, the "proc-receive" hook sends an error message and
dies earlier. While "receive-pack" on the other side of the pipe
should forward the error message of the "proc-receive" hook to the
client side, but there is no such error message in output. It seems
that the expected error message is overridden by the broken pipe error
message. The broken pipe error is because "receive-pack" sends other
commands to the "proc-receive" hook, but the hook dies earlier.

> Applying Gábor's patch as obtained from
> https://lore.kernel.org/git/20190830121005.GI8571@szeder.dev/ seemed to
> help this issue at first, but then turned out not to prevent the same
> issue from happening again.

I think this is the right way to fix this issue, even though I cannot
reproduce this issue in my laptop.

1. In the `run_proc_receive_hook()` function of "receive-pack", should
close the input (proc.in) before reading result from "proc-receive":

--  snip --
@@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct
command *commands,
                packet_flush(proc.in);
        }

+       close(proc.in);
+
        /* Read result from proc-receive */
        code = read_proc_receive_report(&reader, commands, &errmsg);

 cleanup:
-       close(proc.in);
        close(proc.out);
        if (use_sideband)
                finish_async(&muxer);
-- snap --

2. test helper for proc-receive should consume the input stream before
die earlier:

-- snip --
@@ -79,9 +79,11 @@ static void proc_receive_read_commands(struct
packet_reader *reader,
                    *p++ != ' ' ||
                    parse_oid_hex(p, &new_oid, &p) ||
                    *p++ != ' ' ||
-                   die_readline)
+                   die_readline) {
+                       while (packet_reader_read(reader) != PACKET_READ_EOF);
                        die("protocol error: expected 'old new ref', got '%s'",
-                           reader->line);
+                               die_readline? "<call with
--die-readline>": reader->line);
+               }
                refname = p;
                FLEX_ALLOC_STR(cmd, ref_name, refname);
                oidcpy(&cmd->old_oid, &old_oid);
@@ -136,7 +138,7 @@ int cmd__proc_receive(int argc, const char **argv)
                usage_msg_opt("Too many arguments.",
proc_receive_usage, options);
        packet_reader_init(&reader, 0, NULL, 0,
                           PACKET_READ_CHOMP_NEWLINE |
-                          PACKET_READ_DIE_ON_ERR_PACKET);
+                          PACKET_READ_GENTLE_ON_EOF);

        sigchain_push(SIGPIPE, SIG_IGN);
        proc_receive_verison(&reader);
-- snap --

I will send a standalone patch using git-send-email command line later.

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [RFC PATCH] t5411: fix broken pipe write error on proc-receive
  2020-11-05 14:54                                                 ` Jiang Xin
@ 2020-11-05 15:23                                                   ` Jiang Xin
  2020-11-05 19:14                                                     ` Junio C Hamano
  2020-11-05 18:39                                                   ` [PATCH v19 03/10] receive-pack: add new proc-receive hook Junio C Hamano
  1 sibling, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-11-05 15:23 UTC (permalink / raw)
  To: Johannes Schindelin, Junio C Hamano, Git List; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Johannes found a flaky hang in t5411 in the osx-clang job of the CI/PR
builds, and ran into this issue using `--stress` option with the
following errror messages:

    fatal: unable to write flush packet: Broken pipe
    send-pack: unexpected disconnect while reading sideband packet
    fatal: the remote end hung up unexpectedly

In this test case, the "proc-receive" hook sends an error message and
dies earlier. While "receive-pack" on the other side of the pipe
should forward the error message of the "proc-receive" hook to the
client side, but there is no such error message in output. It seems
that the expected error message is overridden by the broken pipe error
message. The broken pipe error is because "receive-pack" sends other
commands to the "proc-receive" hook, but the hook dies earlier.

To fix this issue, these changes are made:

1. In "receive-pack", close the input stream for the "proc-receive" hook
   before reading result from "proc-receive".

2. The test helper for "proc-receive" consumes the input stream before
   it die earlier.

Reported-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c            | 4 +++-
 t/helper/test-proc-receive.c      | 8 +++++---
 t/t5411/test-0013-bad-protocol.sh | 3 +--
 3 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index bb9909c52e..6bd402897c 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1172,6 +1172,7 @@ static int run_proc_receive_hook(struct command *commands,
 	if (version != 1) {
 		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
 			    version);
+		close(proc.in);
 		code = -1;
 		goto cleanup;
 	}
@@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct command *commands,
 		packet_flush(proc.in);
 	}
 
+	close(proc.in);
+
 	/* Read result from proc-receive */
 	code = read_proc_receive_report(&reader, commands, &errmsg);
 
 cleanup:
-	close(proc.in);
 	close(proc.out);
 	if (use_sideband)
 		finish_async(&muxer);
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index 42164d9898..132c74db52 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -79,9 +79,11 @@ static void proc_receive_read_commands(struct packet_reader *reader,
 		    *p++ != ' ' ||
 		    parse_oid_hex(p, &new_oid, &p) ||
 		    *p++ != ' ' ||
-		    die_readline)
+		    die_readline) {
+			while (packet_reader_read(reader) != PACKET_READ_EOF);
 			die("protocol error: expected 'old new ref', got '%s'",
-			    reader->line);
+				die_readline? "<call with --die-readline>": reader->line);
+		}
 		refname = p;
 		FLEX_ALLOC_STR(cmd, ref_name, refname);
 		oidcpy(&cmd->old_oid, &old_oid);
@@ -136,7 +138,7 @@ int cmd__proc_receive(int argc, const char **argv)
 		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
 	packet_reader_init(&reader, 0, NULL, 0,
 			   PACKET_READ_CHOMP_NEWLINE |
-			   PACKET_READ_DIE_ON_ERR_PACKET);
+			   PACKET_READ_GENTLE_ON_EOF);
 
 	sigchain_push(SIGPIPE, SIG_IGN);
 	proc_receive_verison(&reader);
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index c5fe4cb37b..ee75515430 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -91,8 +91,7 @@ test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)
 		HEAD:refs/for/master/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
-
-	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<call with --die-readline>\"" actual &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2020-11-05 14:54                                                 ` Jiang Xin
  2020-11-05 15:23                                                   ` [RFC PATCH] t5411: fix broken pipe write error on proc-receive Jiang Xin
@ 2020-11-05 18:39                                                   ` Junio C Hamano
  1 sibling, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-11-05 18:39 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Johannes Schindelin, Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> I think this is the right way to fix this issue, even though I cannot
> reproduce this issue in my laptop.

Perhaps it depends on the size of the pipe buffer and timing of the
processes getting scheduled?

This is a tangent but I very much like the way comments on a set of
changes below is given interspersed, explaining why the change in
each hunk is done next to it.

> 1. In the `run_proc_receive_hook()` function of "receive-pack", should
> close the input (proc.in) before reading result from "proc-receive":
>
> --  snip --
> @@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct
> command *commands,
>                 packet_flush(proc.in);
>         }
>
> +       close(proc.in);
> +
>         /* Read result from proc-receive */
>         code = read_proc_receive_report(&reader, commands, &errmsg);
>
>  cleanup:
> -       close(proc.in);
>         close(proc.out);
>         if (use_sideband)
>                 finish_async(&muxer);
> -- snap --
>
> 2. test helper for proc-receive should consume the input stream before
> die earlier:
>
> -- snip --
> @@ -79,9 +79,11 @@ static void proc_receive_read_commands(struct
> packet_reader *reader,
>                     *p++ != ' ' ||
>                     parse_oid_hex(p, &new_oid, &p) ||
>                     *p++ != ' ' ||
> -                   die_readline)
> +                   die_readline) {
> +                       while (packet_reader_read(reader) != PACKET_READ_EOF);
>                         die("protocol error: expected 'old new ref', got '%s'",
> -                           reader->line);
> +                               die_readline? "<call with
> --die-readline>": reader->line);
> +               }
>                 refname = p;
>                 FLEX_ALLOC_STR(cmd, ref_name, refname);
>                 oidcpy(&cmd->old_oid, &old_oid);
> @@ -136,7 +138,7 @@ int cmd__proc_receive(int argc, const char **argv)
>                 usage_msg_opt("Too many arguments.",
> proc_receive_usage, options);
>         packet_reader_init(&reader, 0, NULL, 0,
>                            PACKET_READ_CHOMP_NEWLINE |
> -                          PACKET_READ_DIE_ON_ERR_PACKET);
> +                          PACKET_READ_GENTLE_ON_EOF);
>
>         sigchain_push(SIGPIPE, SIG_IGN);
>         proc_receive_verison(&reader);
> -- snap --
>
> I will send a standalone patch using git-send-email command line later.
>
> --
> Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [RFC PATCH] t5411: fix broken pipe write error on proc-receive
  2020-11-05 15:23                                                   ` [RFC PATCH] t5411: fix broken pipe write error on proc-receive Jiang Xin
@ 2020-11-05 19:14                                                     ` Junio C Hamano
  2020-11-07  2:57                                                       ` [PATCH] t5411: consistent result for proc-receive broken test Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-11-05 19:14 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Johannes Schindelin, Git List, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
>
> Johannes found a flaky hang in t5411 in the osx-clang job of the CI/PR
> builds, and ran into this issue using `--stress` option with the
> following errror messages:

s/errror/error/;

>     fatal: unable to write flush packet: Broken pipe
>     send-pack: unexpected disconnect while reading sideband packet
>     fatal: the remote end hung up unexpectedly
>
> In this test case, the "proc-receive" hook sends an error message and
> dies earlier. While "receive-pack" on the other side of the pipe

Micronit.  "dies first" may be easier to read.

> should forward the error message of the "proc-receive" hook to the
> client side, but there is no such error message in output. It seems
> that the expected error message is overridden by the broken pipe error
> message. The broken pipe error is because "receive-pack" sends other
> commands to the "proc-receive" hook, but the hook dies earlier.

The way the exchange designed to happen in a successfull case is
that receive-pack process feeds the commands over the pipe, and
after feeding all commands start reading response?  Even if the
hooks were forbidden from (1) disconnect before reading the commands
and/or (2) speaking before receive-pack finishes feeding the
commands, since they are end-user-supplied random scripts,
receive-pack needs to be prepared to deal with misbehaving hooks.

> To fix this issue, these changes are made:
>
> 1. In "receive-pack", close the input stream for the "proc-receive" hook
>    before reading result from "proc-receive".

This is necessary because...?  Until/unless we close our end, the
hook would not know we finished talking to them, so it is a good
discipline to close our end of the pipe, but it is unclear to me how
this causes the broken pipe failure, i.e. write by receive-pack into
a pipe connected to the hook that has already died.

> 2. The test helper for "proc-receive" consumes the input stream before
>    it die earlier.

"before it dies."

This is merely a workaround in the hook used for testing.  End-user
hook that misbehaves can still disconnect without reading anything,
and receive-pack needs to be prepared for such a case, no?

Is it irrelevant because this is only about forcing a flakey test
that fails in two different ways to fail in a predictable way?

Thanks.

> Reported-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
> Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> ---
>  builtin/receive-pack.c            | 4 +++-
>  t/helper/test-proc-receive.c      | 8 +++++---
>  t/t5411/test-0013-bad-protocol.sh | 3 +--
>  3 files changed, 9 insertions(+), 6 deletions(-)
>
> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index bb9909c52e..6bd402897c 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -1172,6 +1172,7 @@ static int run_proc_receive_hook(struct command *commands,
>  	if (version != 1) {
>  		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
>  			    version);
> +		close(proc.in);
>  		code = -1;
>  		goto cleanup;
>  	}
> @@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct command *commands,
>  		packet_flush(proc.in);
>  	}
>  
> +	close(proc.in);
> +
>  	/* Read result from proc-receive */
>  	code = read_proc_receive_report(&reader, commands, &errmsg);
>  
>  cleanup:
> -	close(proc.in);
>  	close(proc.out);
>  	if (use_sideband)
>  		finish_async(&muxer);
> diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
> index 42164d9898..132c74db52 100644
> --- a/t/helper/test-proc-receive.c
> +++ b/t/helper/test-proc-receive.c
> @@ -79,9 +79,11 @@ static void proc_receive_read_commands(struct packet_reader *reader,
>  		    *p++ != ' ' ||
>  		    parse_oid_hex(p, &new_oid, &p) ||
>  		    *p++ != ' ' ||
> -		    die_readline)
> +		    die_readline) {
> +			while (packet_reader_read(reader) != PACKET_READ_EOF);

Have the empty statement on its own line, i.e.

			while (condition)
				; /* do nothing */

>  			die("protocol error: expected 'old new ref', got '%s'",
> -			    reader->line);
> +				die_readline? "<call with --die-readline>": reader->line);

SP around both sides of "?" and ":", and if that makes the line too
long, consider wrapping the line, i.e.

				die_readline
				? "<call with --die-readline>"
				: reader->line);

> +		}
>  		refname = p;
>  		FLEX_ALLOC_STR(cmd, ref_name, refname);
>  		oidcpy(&cmd->old_oid, &old_oid);
> @@ -136,7 +138,7 @@ int cmd__proc_receive(int argc, const char **argv)
>  		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
>  	packet_reader_init(&reader, 0, NULL, 0,
>  			   PACKET_READ_CHOMP_NEWLINE |
> -			   PACKET_READ_DIE_ON_ERR_PACKET);
> +			   PACKET_READ_GENTLE_ON_EOF);
>  
>  	sigchain_push(SIGPIPE, SIG_IGN);
>  	proc_receive_verison(&reader);
> diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
> index c5fe4cb37b..ee75515430 100644
> --- a/t/t5411/test-0013-bad-protocol.sh
> +++ b/t/t5411/test-0013-bad-protocol.sh
> @@ -91,8 +91,7 @@ test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)
>  		HEAD:refs/for/master/topic \
>  		>out 2>&1 &&
>  	make_user_friendly_and_stable_output <out >actual &&
> -
> -	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
> +	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<call with --die-readline>\"" actual &&
>  
>  	git -C "$upstream" show-ref >out &&
>  	make_user_friendly_and_stable_output <out >actual &&

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH] t5411: consistent result for proc-receive broken test
  2020-11-05 19:14                                                     ` Junio C Hamano
@ 2020-11-07  2:57                                                       ` Jiang Xin
  2020-11-09  7:29                                                         ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-11-07  2:57 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Johannes found a flaky hang in `t5411/test-0013-bad-protocol.sh` in the
osx-clang job of the CI/PR builds, and ran into an issue when using
the `--stress` option with the following error messages:

    fatal: unable to write flush packet: Broken pipe
    send-pack: unexpected disconnect while reading sideband packet
    fatal: the remote end hung up unexpectedly

In this test case, the "proc-receive" hook sends an error message and
dies earlier. While "receive-pack" on the other side of the pipe
should forward the error message of the "proc-receive" hook to the
client side, but it fails to do so. Perhaps it depends on the size of
the pipe buffer and timing of the processes getting scheduled.

The way the exchange designed to happen in a successful case is that
"receive-pack" process feeds the commands, and optional push options
over the pipe, and after feeding all these information, "receive-pack"
starts reading the response of "proc-receive". Let "receive-pack" close
the input stream to "proc-receive" right before reading the response,
so that "proc-receive" may consume all the input from "receive-pack"
before sending an error message and closing the pipe.

The "proc-receive" hook may close the pipe at any time. Three options
(--die-version, --die-readline, and --die-report) can be used for the
test helper to simulate different broken cases. In order to keep the
test results consistent under stress test, only status reports are
matched.

Reported-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c            |  4 ++-
 t/helper/test-proc-receive.c      | 20 +++++++++---
 t/t5411/test-0013-bad-protocol.sh | 53 +++++++++++++++++++++++++------
 3 files changed, 63 insertions(+), 14 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index bb9909c52e..6bd402897c 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1172,6 +1172,7 @@ static int run_proc_receive_hook(struct command *commands,
 	if (version != 1) {
 		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
 			    version);
+		close(proc.in);
 		code = -1;
 		goto cleanup;
 	}
@@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct command *commands,
 		packet_flush(proc.in);
 	}
 
+	close(proc.in);
+
 	/* Read result from proc-receive */
 	code = read_proc_receive_report(&reader, commands, &errmsg);
 
 cleanup:
-	close(proc.in);
 	close(proc.out);
 	if (use_sideband)
 		finish_async(&muxer);
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index 42164d9898..ee5717ba0a 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -12,6 +12,7 @@ static const char *proc_receive_usage[] = {
 
 static int die_version;
 static int die_readline;
+static int die_report;
 static int no_push_options;
 static int use_atomic;
 static int use_push_options;
@@ -52,8 +53,10 @@ static void proc_receive_verison(struct packet_reader *reader) {
 		}
 	}
 
-	if (server_version != 1 || die_version)
+	if (server_version != 1)
 		die("bad protocol version: %d", server_version);
+	if (die_version)
+		die("die with the --die-version option");
 
 	packet_write_fmt(1, "version=%d%c%s\n",
 			 version, '\0',
@@ -75,13 +78,18 @@ static void proc_receive_read_commands(struct packet_reader *reader,
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 
+		if (die_readline)
+			die("die with the --die-readline option");
+
 		if (parse_oid_hex(reader->line, &old_oid, &p) ||
 		    *p++ != ' ' ||
 		    parse_oid_hex(p, &new_oid, &p) ||
-		    *p++ != ' ' ||
-		    die_readline)
+		    *p++ != ' ') {
+			while (packet_reader_read(reader) != PACKET_READ_EOF)
+				; /* do nothing */
 			die("protocol error: expected 'old new ref', got '%s'",
 			    reader->line);
+		}
 		refname = p;
 		FLEX_ALLOC_STR(cmd, ref_name, refname);
 		oidcpy(&cmd->old_oid, &old_oid);
@@ -121,6 +129,8 @@ int cmd__proc_receive(int argc, const char **argv)
 			 "die during version negotiation"),
 		OPT_BOOL(0, "die-readline", &die_readline,
 			 "die when readline"),
+		OPT_BOOL(0, "die-report", &die_report,
+			 "die when reporting"),
 		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
 				"return of results"),
 		OPT__VERBOSE(&verbose, "be verbose"),
@@ -136,7 +146,7 @@ int cmd__proc_receive(int argc, const char **argv)
 		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
 	packet_reader_init(&reader, 0, NULL, 0,
 			   PACKET_READ_CHOMP_NEWLINE |
-			   PACKET_READ_DIE_ON_ERR_PACKET);
+			   PACKET_READ_GENTLE_ON_EOF);
 
 	sigchain_push(SIGPIPE, SIG_IGN);
 	proc_receive_verison(&reader);
@@ -166,6 +176,8 @@ int cmd__proc_receive(int argc, const char **argv)
 				fprintf(stderr, "proc-receive> %s\n", item->string);
 	}
 
+	if (die_report)
+		die("die with the --die-report option");
 	if (returns.nr)
 		for_each_string_list_item(item, &returns)
 			packet_write_fmt(1, "%s\n", item->string);
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index c5fe4cb37b..5c5241bc95 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -55,19 +55,16 @@ test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)"
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/master/topic \
 		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
-
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
 	cat >expect <<-EOF &&
-	remote: # pre-receive hook
-	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
-	remote: # proc-receive hook
-	remote: fatal: bad protocol version: 1
-	remote: error: proc-receive version "0" is not supported
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -90,10 +87,48 @@ test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/master/topic \
 		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
 
-	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+test_expect_success "setup proc-receive hook (hook --die-report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-report
+	EOF
+'
 
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH] t5411: consistent result for proc-receive broken test
  2020-11-07  2:57                                                       ` [PATCH] t5411: consistent result for proc-receive broken test Jiang Xin
@ 2020-11-09  7:29                                                         ` Jiang Xin
  2020-11-09 10:58                                                           ` [PATCH v2] " Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-11-09  7:29 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin; +Cc: Jiang Xin

Jiang Xin <worldhello.net@gmail.com> 于2020年11月7日周六 上午10:57写道:
> @@ -75,13 +78,18 @@ static void proc_receive_read_commands(struct packet_reader *reader,
>                 if (packet_reader_read(reader) != PACKET_READ_NORMAL)
>                         break;
>
> +               if (die_readline)
> +                       die("die with the --die-readline option");
> +
>                 if (parse_oid_hex(reader->line, &old_oid, &p) ||
>                     *p++ != ' ' ||
>                     parse_oid_hex(p, &new_oid, &p) ||
> -                   *p++ != ' ' ||
> -                   die_readline)
> +                   *p++ != ' ') {
> +                       while (packet_reader_read(reader) != PACKET_READ_EOF)
> +                               ; /* do nothing */
>                         die("protocol error: expected 'old new ref', got '%s'",
>                             reader->line);
> +               }
>                 refname = p;
>                 FLEX_ALLOC_STR(cmd, ref_name, refname);
>                 oidcpy(&cmd->old_oid, &old_oid);

Still have problems under the stress test.  Today I figured out how to
run stress test by setting proper environment variable
GIT_TEST_STRESS_LOAD without consuming too many resources and leading
to random strange errors. E.g.:

    $ GIT_TEST_STRESS_LOAD=11 sh  t5411-proc-receive-hook.sh -v --stress

Will send patch v2 later with the following changes:

-- snip --
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index ee5717ba0a..9f7fbc5b7a 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -78,17 +78,18 @@ static void proc_receive_read_commands(struct
packet_reader *reader,
                if (packet_reader_read(reader) != PACKET_READ_NORMAL)
                        break;

-               if (die_readline)
-                       die("die with the --die-readline option");
-
                if (parse_oid_hex(reader->line, &old_oid, &p) ||
                    *p++ != ' ' ||
                    parse_oid_hex(p, &new_oid, &p) ||
-                   *p++ != ' ') {
+                   *p++ != ' ' ||
+                   die_readline) {
+                       char *bad_line = xstrdup(reader->line);
                        while (packet_reader_read(reader) != PACKET_READ_EOF)
                                ; /* do nothing */
+                       if (die_readline)
+                               die("die with the --die-readline option");
                        die("protocol error: expected 'old new ref', got '%s'",
-                           reader->line);
+                           bad_line);
                }
                refname = p;
                FLEX_ALLOC_STR(cmd, ref_name, refname);
-- snap --

^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-09  7:29                                                         ` Jiang Xin
@ 2020-11-09 10:58                                                           ` Jiang Xin
  2020-11-09 20:59                                                             ` Junio C Hamano
  0 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-11-09 10:58 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin; +Cc: Jiang Xin

Johannes found a flaky hang in `t5411/test-0013-bad-protocol.sh` in the
osx-clang job of the CI/PR builds, and ran into an issue when using
the `--stress` option with the following error messages:

    fatal: unable to write flush packet: Broken pipe
    send-pack: unexpected disconnect while reading sideband packet
    fatal: the remote end hung up unexpectedly

In this test case, the "proc-receive" hook sends an error message and
dies earlier. While "receive-pack" on the other side of the pipe
should forward the error message of the "proc-receive" hook to the
client side, but it fails to do so. Perhaps it depends on the size of
the pipe buffer and timing of the processes getting scheduled.

The way the exchange designed to happen in a successful case is that
"receive-pack" process feeds the commands, and optional push options
over the pipe, and after feeding all these information, "receive-pack"
starts reading the response of "proc-receive". Let "receive-pack" close
the input stream to "proc-receive" right before reading the response,
so that "proc-receive" may consume all the input from "receive-pack"
before sending an error message and closing the pipe.

The "proc-receive" hook may close the pipe at any time. Three options
(--die-version, --die-readline, and --die-report) can be used for the
test helper to simulate different broken cases. In order to keep the
test results consistent under stress test, only status reports are
matched.

Reported-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                       |  4 +-
 t/helper/test-proc-receive.c                 | 21 ++++-
 t/t5411/test-0013-bad-protocol.sh            | 53 +++++++++--
 t/t5411/test-0014-bad-protocol--porcelain.sh | 96 ++++++++++++++++++++
 4 files changed, 160 insertions(+), 14 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index bb9909c52e..6bd402897c 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1172,6 +1172,7 @@ static int run_proc_receive_hook(struct command *commands,
 	if (version != 1) {
 		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
 			    version);
+		close(proc.in);
 		code = -1;
 		goto cleanup;
 	}
@@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct command *commands,
 		packet_flush(proc.in);
 	}
 
+	close(proc.in);
+
 	/* Read result from proc-receive */
 	code = read_proc_receive_report(&reader, commands, &errmsg);
 
 cleanup:
-	close(proc.in);
 	close(proc.out);
 	if (use_sideband)
 		finish_async(&muxer);
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index 42164d9898..9f7fbc5b7a 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -12,6 +12,7 @@ static const char *proc_receive_usage[] = {
 
 static int die_version;
 static int die_readline;
+static int die_report;
 static int no_push_options;
 static int use_atomic;
 static int use_push_options;
@@ -52,8 +53,10 @@ static void proc_receive_verison(struct packet_reader *reader) {
 		}
 	}
 
-	if (server_version != 1 || die_version)
+	if (server_version != 1)
 		die("bad protocol version: %d", server_version);
+	if (die_version)
+		die("die with the --die-version option");
 
 	packet_write_fmt(1, "version=%d%c%s\n",
 			 version, '\0',
@@ -79,9 +82,15 @@ static void proc_receive_read_commands(struct packet_reader *reader,
 		    *p++ != ' ' ||
 		    parse_oid_hex(p, &new_oid, &p) ||
 		    *p++ != ' ' ||
-		    die_readline)
+		    die_readline) {
+			char *bad_line = xstrdup(reader->line);
+			while (packet_reader_read(reader) != PACKET_READ_EOF)
+				; /* do nothing */
+			if (die_readline)
+				die("die with the --die-readline option");
 			die("protocol error: expected 'old new ref', got '%s'",
-			    reader->line);
+			    bad_line);
+		}
 		refname = p;
 		FLEX_ALLOC_STR(cmd, ref_name, refname);
 		oidcpy(&cmd->old_oid, &old_oid);
@@ -121,6 +130,8 @@ int cmd__proc_receive(int argc, const char **argv)
 			 "die during version negotiation"),
 		OPT_BOOL(0, "die-readline", &die_readline,
 			 "die when readline"),
+		OPT_BOOL(0, "die-report", &die_report,
+			 "die when reporting"),
 		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
 				"return of results"),
 		OPT__VERBOSE(&verbose, "be verbose"),
@@ -136,7 +147,7 @@ int cmd__proc_receive(int argc, const char **argv)
 		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
 	packet_reader_init(&reader, 0, NULL, 0,
 			   PACKET_READ_CHOMP_NEWLINE |
-			   PACKET_READ_DIE_ON_ERR_PACKET);
+			   PACKET_READ_GENTLE_ON_EOF);
 
 	sigchain_push(SIGPIPE, SIG_IGN);
 	proc_receive_verison(&reader);
@@ -166,6 +177,8 @@ int cmd__proc_receive(int argc, const char **argv)
 				fprintf(stderr, "proc-receive> %s\n", item->string);
 	}
 
+	if (die_report)
+		die("die with the --die-report option");
 	if (returns.nr)
 		for_each_string_list_item(item, &returns)
 			packet_write_fmt(1, "%s\n", item->string);
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index c5fe4cb37b..5c5241bc95 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -55,19 +55,16 @@ test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)"
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/master/topic \
 		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
-
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
 	cat >expect <<-EOF &&
-	remote: # pre-receive hook
-	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/master/topic
-	remote: # proc-receive hook
-	remote: fatal: bad protocol version: 1
-	remote: error: proc-receive version "0" is not supported
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/master/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -90,10 +87,48 @@ test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/master/topic \
 		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
 
-	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/master/topic\"" actual &&
+test_expect_success "setup proc-receive hook (hook --die-report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-report
+	EOF
+'
 
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { s/   */ /g; p; }" \
+			-e "/^ ! / { s/   */ /g; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/master/topic (proc-receive failed to report status)
+	EOF
+	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index 53b47b0185..df19777ab0 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -42,6 +42,102 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porc
 	test_cmp expect actual
 '
 
+test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-version
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { p; n; p; n; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-readline
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { p; n; p; n; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-report, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-report
+	EOF
+'
+
+# Refs of upstream : master(A)
+# Refs of workbench: master(A)  tags/v123
+# git push         :                       refs/for/master/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/master/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out |
+		sed -n \
+			-e "/^To / { p; n; p; n; p; }" \
+			>actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/master/topic    [remote rejected] (proc-receive failed to report status)
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/master
+	EOF
+	test_cmp expect actual
+'
+
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-09 10:58                                                           ` [PATCH v2] " Jiang Xin
@ 2020-11-09 20:59                                                             ` Junio C Hamano
  2020-11-09 23:12                                                               ` Jeff King
  2020-11-10 11:44                                                               ` [PATCH v2] t5411: consistent result for proc-receive broken test Jiang Xin
  0 siblings, 2 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-11-09 20:59 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Johannes Schindelin, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index bb9909c52e..6bd402897c 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -1172,6 +1172,7 @@ static int run_proc_receive_hook(struct command *commands,
>  	if (version != 1) {
>  		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
>  			    version);
> +		close(proc.in);
>  		code = -1;
>  		goto cleanup;
>  	}
> @@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct command *commands,
>  		packet_flush(proc.in);
>  	}
>  
> +	close(proc.in);
> +
>  	/* Read result from proc-receive */
>  	code = read_proc_receive_report(&reader, commands, &errmsg);
>  
>  cleanup:
> -	close(proc.in);
>  	close(proc.out);
>  	if (use_sideband)
>  		finish_async(&muxer);

OK, without us closing our end, the hook cannot even tell that it
read to the end of our input.

> diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
> index 42164d9898..9f7fbc5b7a 100644
> --- a/t/helper/test-proc-receive.c
> +++ b/t/helper/test-proc-receive.c
> @@ -12,6 +12,7 @@ static const char *proc_receive_usage[] = {
>  
>  static int die_version;
>  static int die_readline;
> +static int die_report;
>  static int no_push_options;
>  static int use_atomic;
>  static int use_push_options;
> @@ -52,8 +53,10 @@ static void proc_receive_verison(struct packet_reader *reader) {
>  		}
>  	}
>  
> -	if (server_version != 1 || die_version)
> +	if (server_version != 1)
>  		die("bad protocol version: %d", server_version);
> +	if (die_version)
> +		die("die with the --die-version option");

If any of these trigger, wouldn't we end up dying without consuming
what receive-pack said?

>  
>  	packet_write_fmt(1, "version=%d%c%s\n",
>  			 version, '\0',
> @@ -79,9 +82,15 @@ static void proc_receive_read_commands(struct packet_reader *reader,
>  		    *p++ != ' ' ||
>  		    parse_oid_hex(p, &new_oid, &p) ||
>  		    *p++ != ' ' ||
> -		    die_readline)
> +		    die_readline) {
> +			char *bad_line = xstrdup(reader->line);
> +			while (packet_reader_read(reader) != PACKET_READ_EOF)
> +				; /* do nothing */
> +			if (die_readline)
> +				die("die with the --die-readline option");
>  			die("protocol error: expected 'old new ref', got '%s'",
> -			    reader->line);
> +			    bad_line);
> +		}

This part is different from the previous one in that it slurps all
the input before dying evein in die_readline case.

> @@ -166,6 +177,8 @@ int cmd__proc_receive(int argc, const char **argv)
>  				fprintf(stderr, "proc-receive> %s\n", item->string);
>  	}
>  
> +	if (die_report)
> +		die("die with the --die-report option");

And at this point we have already read everything the other end
said (if so, there is no need for the artificial "read everything
before we die")?

> diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
> index c5fe4cb37b..5c5241bc95 100644
> --- a/t/t5411/test-0013-bad-protocol.sh
> +++ b/t/t5411/test-0013-bad-protocol.sh
> @@ -55,19 +55,16 @@ test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)"
>  	test_must_fail git -C workbench push origin \
>  		HEAD:refs/for/master/topic \
>  		>out 2>&1 &&

Are these expected to conflict with Dscho's changes to move 'master'
around?

> -	make_user_friendly_and_stable_output <out >actual &&
> -
> +	make_user_friendly_and_stable_output <out |
> +		sed -n \
> +			-e "/^To / { s/   */ /g; p; }" \
> +			-e "/^ ! / { s/   */ /g; p; }" \
> +			>actual &&

It's the same thing but I somehow find "s/  */ /g" easier to read.
The comparison is between "there may be two things or more---squish
them down to one" and "After one thing, there may be any number of
things---remove all the extra ones".

Makes me wonder if make_user_friendly should optionally have an
option to do something like this for us.  I doubt it that it is
worth to do something like the attached patch.


 t/t5411/common-functions.sh | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git c/t/t5411/common-functions.sh w/t/t5411/common-functions.sh
index 6580bebd8e..6919639c60 100644
--- c/t/t5411/common-functions.sh
+++ w/t/t5411/common-functions.sh
@@ -40,7 +40,9 @@ create_commits_in () {
 # `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
 # on plumbing commands.
 make_user_friendly_and_stable_output () {
-	sed \
+	local en=
+	case "$#" in 0) ;; *) en=-n ;; esac
+	sed $en \
 		-e "s/  *\$//" \
 		-e "s/   */ /g" \
 		-e "s/'/\"/g" \
@@ -52,5 +54,6 @@ make_user_friendly_and_stable_output () {
 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
 		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
 		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
-		-e "/^error: / d"
+		-e "/^error: / d" \
+		${1+"$@"}
 }

^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-09 20:59                                                             ` Junio C Hamano
@ 2020-11-09 23:12                                                               ` Jeff King
  2020-11-09 23:22                                                                 ` Junio C Hamano
  2020-11-10 11:49                                                                 ` Jiang Xin
  2020-11-10 11:44                                                               ` [PATCH v2] t5411: consistent result for proc-receive broken test Jiang Xin
  1 sibling, 2 replies; 266+ messages in thread
From: Jeff King @ 2020-11-09 23:12 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Jiang Xin, Git List, Johannes Schindelin, Jiang Xin

On Mon, Nov 09, 2020 at 12:59:24PM -0800, Junio C Hamano wrote:

> > @@ -1196,11 +1197,12 @@ static int run_proc_receive_hook(struct command *commands,
> >  		packet_flush(proc.in);
> >  	}
> >  
> > +	close(proc.in);
> > +
> >  	/* Read result from proc-receive */
> >  	code = read_proc_receive_report(&reader, commands, &errmsg);
> >  
> >  cleanup:
> > -	close(proc.in);
> >  	close(proc.out);
> >  	if (use_sideband)
> >  		finish_async(&muxer);
> 
> OK, without us closing our end, the hook cannot even tell that it
> read to the end of our input.

That doesn't seem right. It should be expecting our flush packet,
shouldn't it? And if it sees an EOF before the flush packet, that would
be an error from the hook's perspective.

This part of the patch seems like a red herring to me.

> > @@ -52,8 +53,10 @@ static void proc_receive_verison(struct packet_reader *reader) {
> >  		}
> >  	}
> >  
> > -	if (server_version != 1 || die_version)
> > +	if (server_version != 1)
> >  		die("bad protocol version: %d", server_version);
> > +	if (die_version)
> > +		die("die with the --die-version option");
> 
> If any of these trigger, wouldn't we end up dying without consuming
> what receive-pack said?

Yeah, I think they would have the same race that the commit message
describes (proc-receive hook writes to stderr and dies, receive-pack
gets an error writing to now-closed hook pipe and never relays the
stderr).

But it seems like fixing this in the hook is the wrong place. The hook
has failed and has nothing else to say. Adding a pump-the-stdin-to-eof
loop to every die() is a lot of effort. Not to mention that the hook
could fail for reasons outside its usual flow control (e.g., segfault,
oom, etc, and receive-pack should be able to handle that gracefully,
even if the hook doesn't appear to behave.

I.e., I think the bug is in receive-pack's run_proc_receive_hook(). It
cleverly ignores SIGPIPE exactly to avoid dying during the write phase,
but then it proceeds to call packet_write_fmt(), etc, that will die on
any error (going to extra effort to emulate sigpipe, no less!). So we
die and take our sideband muxer with us.

So instead of this hunk:

> > @@ -79,9 +82,15 @@ static void proc_receive_read_commands(struct packet_reader *reader,
> >  		    *p++ != ' ' ||
> >  		    parse_oid_hex(p, &new_oid, &p) ||
> >  		    *p++ != ' ' ||
> > -		    die_readline)
> > +		    die_readline) {
> > +			char *bad_line = xstrdup(reader->line);
> > +			while (packet_reader_read(reader) != PACKET_READ_EOF)
> > +				; /* do nothing */
> > +			if (die_readline)
> > +				die("die with the --die-readline option");
> >  			die("protocol error: expected 'old new ref', got '%s'",
> > -			    reader->line);
> > +			    bad_line);
> > +		}
> 
> This part is different from the previous one in that it slurps all
> the input before dying evein in die_readline case.

I think the patch really ought to be in receive-pack, converting
packet_write_fmt() and packet_flush() into their "gently" forms.

And when we see a write error, close our pipe to the hook, set errmsg to
"hook failed to run" or similar, and then jump to the "cleanup" label,
where we'll wait on our sideband muxer to finish (which in turn will
wait pump any remaining data out of the hook's stderr).

Optionally we can pump the hook stdout to see if it gave us a better
message, but I think if write() failed, then all bets are off. The hook
broke protocol; a well-behaved hook that wanted to pass along a specific
per-ref message to the user would actually read all the input and then
report on each ref).

-Peff

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-09 23:12                                                               ` Jeff King
@ 2020-11-09 23:22                                                                 ` Junio C Hamano
  2020-11-10  0:03                                                                   ` Jeff King
  2020-11-10 11:49                                                                 ` Jiang Xin
  1 sibling, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-11-09 23:22 UTC (permalink / raw)
  To: Jeff King; +Cc: Jiang Xin, Git List, Johannes Schindelin, Jiang Xin

Jeff King <peff@peff.net> writes:

> I.e., I think the bug is in receive-pack's run_proc_receive_hook().

I thought that this was merely to work it around and give the
particular test a reliable failure code, though.  Either way,
receive-pack will get a failure when the hook fails (either the
failure message from the hook, or unexpected EOF), so no matter what
this patch does around this area, it would not affect interactions
with real-life hooks, I would think.

> It
> cleverly ignores SIGPIPE exactly to avoid dying during the write phase,
> but then it proceeds to call packet_write_fmt(), etc, that will die on
> any error (going to extra effort to emulate sigpipe, no less!). So we
> die and take our sideband muxer with us.
>
> So instead of this hunk:
>
>> > @@ -79,9 +82,15 @@ static void proc_receive_read_commands(struct packet_reader *reader,
>> >  		    *p++ != ' ' ||
>> >  		    parse_oid_hex(p, &new_oid, &p) ||
>> >  		    *p++ != ' ' ||
>> > -		    die_readline)
>> > +		    die_readline) {
>> > +			char *bad_line = xstrdup(reader->line);
>> > +			while (packet_reader_read(reader) != PACKET_READ_EOF)
>> > +				; /* do nothing */
>> > +			if (die_readline)
>> > +				die("die with the --die-readline option");
>> >  			die("protocol error: expected 'old new ref', got '%s'",
>> > -			    reader->line);
>> > +			    bad_line);
>> > +		}
>> 
>> This part is different from the previous one in that it slurps all
>> the input before dying evein in die_readline case.
>
> I think the patch really ought to be in receive-pack, converting
> packet_write_fmt() and packet_flush() into their "gently" forms.

That would be a real fix, not "work around flakey tests".  Yes, the
hook driver roughly mimics run_and_feed_hook() and I suspect it
started from copy-and-paste from that function, so we might need to
review that original copy, too.

> And when we see a write error, close our pipe to the hook, set errmsg to
> "hook failed to run" or similar, and then jump to the "cleanup" label,
> where we'll wait on our sideband muxer to finish (which in turn will
> wait pump any remaining data out of the hook's stderr).
>
> Optionally we can pump the hook stdout to see if it gave us a better
> message, but I think if write() failed, then all bets are off. The hook
> broke protocol; a well-behaved hook that wanted to pass along a specific
> per-ref message to the user would actually read all the input and then
> report on each ref).

Yup, sounds good.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-09 23:22                                                                 ` Junio C Hamano
@ 2020-11-10  0:03                                                                   ` Jeff King
  0 siblings, 0 replies; 266+ messages in thread
From: Jeff King @ 2020-11-10  0:03 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Jiang Xin, Git List, Johannes Schindelin, Jiang Xin

On Mon, Nov 09, 2020 at 03:22:32PM -0800, Junio C Hamano wrote:

> Jeff King <peff@peff.net> writes:
> 
> > I.e., I think the bug is in receive-pack's run_proc_receive_hook().
> 
> I thought that this was merely to work it around and give the
> particular test a reliable failure code, though.  Either way,
> receive-pack will get a failure when the hook fails (either the
> failure message from the hook, or unexpected EOF), so no matter what
> this patch does around this area, it would not affect interactions
> with real-life hooks, I would think.

Yes. Either way receive-pack will fail and won't update any refs, which
is good. The question is whether or not it gives a useful message back
to the client or not. So the tests are exposing an interesting
user-visible behavior, albeit one which we wouldn't expect to come up
much in practice.

> > I think the patch really ought to be in receive-pack, converting
> > packet_write_fmt() and packet_flush() into their "gently" forms.
> 
> That would be a real fix, not "work around flakey tests".  Yes, the
> hook driver roughly mimics run_and_feed_hook() and I suspect it
> started from copy-and-paste from that function, so we might need to
> review that original copy, too.

Good idea. I think it is fine, though. It does all of its writing
through write_in_full(), and breaks out of the write loop on error.
Whereas run_proc_receive_hook() switched to using the packet_*
functions, which by default die on write errors. So I think that's the
root of the problem.

I found it a little curious that run_and_feed_hook() did not complain on
write error, but I think it is explicitly allowing a hook to "exit 0"
immediately without even reading the input. As long as the hook exits
non-zero, we'd still consider that an error (and because we handled the
write error gracefully, we'd do so reliably).  It would be weird if we
saw some _other_ error (EIO or something) while writing, though. I
suspect that's all but impossible for a pipe write like this, but it
wouldn't be wrong to tighten it up, I suppose.

And back to run_proc_receive_hook(), I think it _should_ complain about
a write error, rather than allowing the hook to quietly say "exit 0".
The protocol for the proc-receive hook is more complicated, and we are
looking for an exchange of pkt-lines, and not just "we dumped some data
and it gave us an exit code". So I think it failing to read should
probably be considered a break of that pkt-line protocol.

-Peff

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-09 20:59                                                             ` Junio C Hamano
  2020-11-09 23:12                                                               ` Jeff King
@ 2020-11-10 11:44                                                               ` Jiang Xin
  1 sibling, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-10 11:44 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Johannes Schindelin, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年11月10日周二 上午4:59写道:
>
> > @@ -166,6 +177,8 @@ int cmd__proc_receive(int argc, const char **argv)
> >                               fprintf(stderr, "proc-receive> %s\n", item->string);
> >       }
> >
> > +     if (die_report)
> > +             die("die with the --die-report option");
>
> And at this point we have already read everything the other end
> said (if so, there is no need for the artificial "read everything
> before we die")?

In patch v3, will use "gently" forms of packet_write_fmt() and
packet_flush(), and it is no necessary to read everything before die.
And will add more "--die-*" option in test helper.

> > diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
> > index c5fe4cb37b..5c5241bc95 100644
> > --- a/t/t5411/test-0013-bad-protocol.sh
> > +++ b/t/t5411/test-0013-bad-protocol.sh
> > @@ -55,19 +55,16 @@ test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)"
> >       test_must_fail git -C workbench push origin \
> >               HEAD:refs/for/master/topic \
> >               >out 2>&1 &&
>
> Are these expected to conflict with Dscho's changes to move 'master'
> around?

See topic "gitster/js/default-branch-name-adjust-t5411" has been merge
to master branch already. Will rebase on it.

> > -     make_user_friendly_and_stable_output <out >actual &&
> > -
> > +     make_user_friendly_and_stable_output <out |
> > +             sed -n \
> > +                     -e "/^To / { s/   */ /g; p; }" \
> > +                     -e "/^ ! / { s/   */ /g; p; }" \
> > +                     >actual &&
>
> It's the same thing but I somehow find "s/  */ /g" easier to read.
> The comparison is between "there may be two things or more---squish
> them down to one" and "After one thing, there may be any number of
> things---remove all the extra ones".
>
> Makes me wonder if make_user_friendly should optionally have an
> option to do something like this for us.  I doubt it that it is
> worth to do something like the attached patch.
>
>
>  t/t5411/common-functions.sh | 7 +++++--
>  1 file changed, 5 insertions(+), 2 deletions(-)
>
> diff --git c/t/t5411/common-functions.sh w/t/t5411/common-functions.sh
> index 6580bebd8e..6919639c60 100644
> --- c/t/t5411/common-functions.sh
> +++ w/t/t5411/common-functions.sh
> @@ -40,7 +40,9 @@ create_commits_in () {
>  # `GIT_TEST_GETTEXT_POISON=true` in order to test unintentional translations
>  # on plumbing commands.
>  make_user_friendly_and_stable_output () {
> -       sed \
> +       local en=
> +       case "$#" in 0) ;; *) en=-n ;; esac
> +       sed $en \
>                 -e "s/  *\$//" \
>                 -e "s/   */ /g" \
>                 -e "s/'/\"/g" \
> @@ -52,5 +54,6 @@ make_user_friendly_and_stable_output () {
>                 -e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
>                 -e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
>                 -e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
> -               -e "/^error: / d"
> +               -e "/^error: / d" \
> +               ${1+"$@"}
>  }

If we call make_user_friendly_and_stable_output like this:

    make_user_friendly_and_stable_output \
         -e "/^To / { p; n; p; n; p; }

Text lines next to "^To " will not be formatted by the built-in sed's
arguments. So will write like this:

-- snip --
diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 521a347710..b7cca2d8fb 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -42,7 +42,7 @@ create_commits_in () {
 make_user_friendly_and_stable_output () {
        sed \
                -e "s/  *\$//" \
-               -e "s/   */ /g" \
+               -e "s/  */ /g" \
                -e "s/'/\"/g" \
                -e "s/  /    /g" \
                -e "s/$A/<COMMIT-A>/g" \
@@ -52,5 +52,11 @@ make_user_friendly_and_stable_output () {
                -e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
                -e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
                -e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
-               -e "/^error: / d"
+               -e "/^error: / d" | \
+       if test $# -eq 0
+       then
+               cat
+       else
+               sed ${1+"$@"}
+       fi
 }
-- snap --

^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-09 23:12                                                               ` Jeff King
  2020-11-09 23:22                                                                 ` Junio C Hamano
@ 2020-11-10 11:49                                                                 ` Jiang Xin
  2020-11-10 12:01                                                                   ` [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output Jiang Xin
                                                                                     ` (6 more replies)
  1 sibling, 7 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-10 11:49 UTC (permalink / raw)
  To: Jeff King; +Cc: Junio C Hamano, Git List, Johannes Schindelin, Jiang Xin

Jeff King <peff@peff.net> 于2020年11月10日周二 上午7:12写道:
> > > @@ -52,8 +53,10 @@ static void proc_receive_verison(struct packet_reader *reader) {
> > >             }
> > >     }
> > >
> > > -   if (server_version != 1 || die_version)
> > > +   if (server_version != 1)
> > >             die("bad protocol version: %d", server_version);
> > > +   if (die_version)
> > > +           die("die with the --die-version option");
> >
> > If any of these trigger, wouldn't we end up dying without consuming
> > what receive-pack said?
>
> Yeah, I think they would have the same race that the commit message
> describes (proc-receive hook writes to stderr and dies, receive-pack
> gets an error writing to now-closed hook pipe and never relays the
> stderr).
>
> But it seems like fixing this in the hook is the wrong place. The hook
> has failed and has nothing else to say. Adding a pump-the-stdin-to-eof
> loop to every die() is a lot of effort. Not to mention that the hook
> could fail for reasons outside its usual flow control (e.g., segfault,
> oom, etc, and receive-pack should be able to handle that gracefully,
> even if the hook doesn't appear to behave.
>
> I.e., I think the bug is in receive-pack's run_proc_receive_hook(). It
> cleverly ignores SIGPIPE exactly to avoid dying during the write phase,
> but then it proceeds to call packet_write_fmt(), etc, that will die on
> any error (going to extra effort to emulate sigpipe, no less!). So we
> die and take our sideband muxer with us.
>
> So instead of this hunk:
>
> > > @@ -79,9 +82,15 @@ static void proc_receive_read_commands(struct packet_reader *reader,
> > >                 *p++ != ' ' ||
> > >                 parse_oid_hex(p, &new_oid, &p) ||
> > >                 *p++ != ' ' ||
> > > -               die_readline)
> > > +               die_readline) {
> > > +                   char *bad_line = xstrdup(reader->line);
> > > +                   while (packet_reader_read(reader) != PACKET_READ_EOF)
> > > +                           ; /* do nothing */
> > > +                   if (die_readline)
> > > +                           die("die with the --die-readline option");
> > >                     die("protocol error: expected 'old new ref', got '%s'",
> > > -                       reader->line);
> > > +                       bad_line);
> > > +           }
> >
> > This part is different from the previous one in that it slurps all
> > the input before dying evein in die_readline case.
>
> I think the patch really ought to be in receive-pack, converting
> packet_write_fmt() and packet_flush() into their "gently" forms.

Thanks Peff for pointing the root cause. Will use the "gently" forms
of packet_write_fmt() and packet_flush() in patch v3.

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output
  2020-11-10 11:49                                                                 ` Jiang Xin
@ 2020-11-10 12:01                                                                   ` Jiang Xin
  2020-11-10 20:51                                                                     ` Junio C Hamano
  2020-11-10 12:01                                                                   ` [PATCH v3 2/2] receive-pack: gently write messages to proc-receive Jiang Xin
                                                                                     ` (5 subsequent siblings)
  6 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-11-10 12:01 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin, Jeff King; +Cc: Jiang Xin

Common function `make_user_friendly_and_stable_output()` can take
additional arguments to format messages for special cases.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/common-functions.sh                       | 10 ++++++++--
 t/t5411/test-0000-standard-git-push.sh            |  9 ++++-----
 t/t5411/test-0001-standard-git-push--porcelain.sh | 15 +++++++--------
 3 files changed, 19 insertions(+), 15 deletions(-)

diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 521a347710..b7cca2d8fb 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -42,7 +42,7 @@ create_commits_in () {
 make_user_friendly_and_stable_output () {
 	sed \
 		-e "s/  *\$//" \
-		-e "s/   */ /g" \
+		-e "s/  */ /g" \
 		-e "s/'/\"/g" \
 		-e "s/	/    /g" \
 		-e "s/$A/<COMMIT-A>/g" \
@@ -52,5 +52,11 @@ make_user_friendly_and_stable_output () {
 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
 		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
 		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
-		-e "/^error: / d"
+		-e "/^error: / d" | \
+	if test $# -eq 0
+	then
+		cat
+	else
+		sed ${1+"$@"}
+	fi
 }
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index 2b04b49367..b3af3f59b0 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -36,11 +36,10 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 		main \
 		$B:refs/heads/next \
 		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out |
-		sed -n \
-			-e "/^To / { s/   */ /g; p; }" \
-			-e "/^ ! / { s/   */ /g; p; }" \
-			>actual &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [rejected] main -> main (non-fast-forward)
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index 747307f8da..16ff2d5666 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -37,16 +37,15 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 		main \
 		$B:refs/heads/next \
 		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out |
-		sed -n \
-			-e "s/^# GETTEXT POISON #//" \
-			-e "/^To / { s/   */ /g; p; }" \
-			-e "/^! / { s/   */ /g; p; }" \
-			>actual &&
+	make_user_friendly_and_stable_output -n \
+		-e "s/^# GETTEXT POISON #//" \
+		-e "/^To / { p; }" \
+		-e "/^! / { p; }" \
+		<out >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
-	! refs/heads/main:refs/heads/main [rejected] (non-fast-forward)
-	! <COMMIT-B>:refs/heads/next [rejected] (atomic push failed)
+	!    refs/heads/main:refs/heads/main    [rejected] (non-fast-forward)
+	!    <COMMIT-B>:refs/heads/next    [rejected] (atomic push failed)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 2/2] receive-pack: gently write messages to proc-receive
  2020-11-10 11:49                                                                 ` Jiang Xin
  2020-11-10 12:01                                                                   ` [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output Jiang Xin
@ 2020-11-10 12:01                                                                   ` Jiang Xin
  2020-11-10 21:52                                                                     ` Jeff King
  2020-11-10 21:00                                                                   ` [PATCH v2] t5411: consistent result for proc-receive broken test Junio C Hamano
                                                                                     ` (4 subsequent siblings)
  6 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2020-11-10 12:01 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin, Jeff King; +Cc: Jiang Xin

Johannes found a flaky hang in `t5411/test-0013-bad-protocol.sh` in the
osx-clang job of the CI/PR builds, and ran into an issue when using
the `--stress` option with the following error messages:

    fatal: unable to write flush packet: Broken pipe
    send-pack: unexpected disconnect while reading sideband packet
    fatal: the remote end hung up unexpectedly

In this test case, the "proc-receive" hook sends an error message and
dies earlier. While "receive-pack" on the other side of the pipe
should forward the error message of the "proc-receive" hook to the
client side, but it fails to do so. This is because "receive-pack"
uses `packet_write_fmt()` and `packet_flush()` to write pkt-line
message to "proc-receive" hook, and these functions die immediately
when pipe is broken. Using "gently" forms for these functions will get
more predicable output.

Add more "--die-*" options to test helper to test different stages of
the protocol between "receive-pack" and "proc-receive" hook.

Reported-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                       |  62 +++++--
 t/helper/test-proc-receive.c                 |  42 +++--
 t/t5411/test-0013-bad-protocol.sh            | 141 +++++++++++++--
 t/t5411/test-0014-bad-protocol--porcelain.sh | 176 ++++++++++++++++++-
 4 files changed, 380 insertions(+), 41 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index bb9909c52e..697a4e8802 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -974,9 +974,10 @@ static int read_proc_receive_report(struct packet_reader *reader,
 	struct command *cmd;
 	struct command *hint = NULL;
 	struct ref_push_report *report = NULL;
-	int new_report = 0;
 	int code = 0;
+	int new_report = 0;
 	int once = 0;
+	int response = 0;
 
 	for (;;) {
 		struct object_id old_oid, new_oid;
@@ -984,8 +985,14 @@ static int read_proc_receive_report(struct packet_reader *reader,
 		const char *refname;
 		char *p;
 
-		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			if (!response) {
+				strbuf_addstr(errmsg, "no response from proc-receive hook");
+				return -1;
+			}
 			break;
+		}
+		response++;
 
 		head = reader->line;
 		p = strchr(head, ' ');
@@ -1100,7 +1107,7 @@ static int run_proc_receive_hook(struct command *commands,
 	struct strbuf cap = STRBUF_INIT;
 	struct strbuf errmsg = STRBUF_INIT;
 	int hook_use_push_options = 0;
-	int version = 0;
+	int version = -1;
 	int code;
 
 	argv[0] = find_hook("proc-receive");
@@ -1145,12 +1152,17 @@ static int run_proc_receive_hook(struct command *commands,
 	if (use_push_options)
 		strbuf_addstr(&cap, " push-options");
 	if (cap.len) {
-		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		code = packet_write_fmt_gently(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
 		strbuf_release(&cap);
 	} else {
-		packet_write_fmt(proc.in, "version=1\n");
+		code = packet_write_fmt_gently(proc.in, "version=1\n");
+	}
+	if (!code)
+		code = packet_flush_gently(proc.in);
+	if (code) {
+		strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook");
+		goto cleanup;
 	}
-	packet_flush(proc.in);
 
 	for (;;) {
 		int linelen;
@@ -1169,7 +1181,11 @@ static int run_proc_receive_hook(struct command *commands,
 		}
 	}
 
-	if (version != 1) {
+	if (version == -1) {
+		strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook");
+		code = -1;
+		goto cleanup;
+	} else if (version != 1) {
 		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
 			    version);
 		code = -1;
@@ -1180,20 +1196,36 @@ static int run_proc_receive_hook(struct command *commands,
 	for (cmd = commands; cmd; cmd = cmd->next) {
 		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
 			continue;
-		packet_write_fmt(proc.in, "%s %s %s",
-				 oid_to_hex(&cmd->old_oid),
-				 oid_to_hex(&cmd->new_oid),
-				 cmd->ref_name);
+		code = packet_write_fmt_gently(proc.in, "%s %s %s",
+					       oid_to_hex(&cmd->old_oid),
+					       oid_to_hex(&cmd->new_oid),
+					       cmd->ref_name);
+		if (code)
+			break;
+	}
+	if (!code)
+		code = packet_flush_gently(proc.in);
+	if (code) {
+		strbuf_addstr(&errmsg, "fail to write commands to proc-receive hook");
+		goto cleanup;
 	}
-	packet_flush(proc.in);
 
 	/* Send push options */
 	if (hook_use_push_options) {
 		struct string_list_item *item;
 
-		for_each_string_list_item(item, push_options)
-			packet_write_fmt(proc.in, "%s", item->string);
-		packet_flush(proc.in);
+		for_each_string_list_item(item, push_options) {
+			code = packet_write_fmt_gently(proc.in, "%s", item->string);
+			if (code)
+				break;
+		}
+		if (!code)
+			code = packet_flush_gently(proc.in);
+		if (code) {
+			strbuf_addstr(&errmsg,
+				      "fail to write push-options to proc-receive hook");
+			goto cleanup;
+		}
 	}
 
 	/* Read result from proc-receive */
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index 42164d9898..6652cedcee 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -10,8 +10,11 @@ static const char *proc_receive_usage[] = {
 	NULL
 };
 
-static int die_version;
-static int die_readline;
+static int die_read_version;
+static int die_write_version;
+static int die_read_commands;
+static int die_read_push_options;
+static int die_write_report;
 static int no_push_options;
 static int use_atomic;
 static int use_push_options;
@@ -33,6 +36,9 @@ struct command {
 static void proc_receive_verison(struct packet_reader *reader) {
 	int server_version = 0;
 
+	if (die_read_version)
+		die("die with the --die-read-version option");
+
 	for (;;) {
 		int linelen;
 
@@ -52,9 +58,12 @@ static void proc_receive_verison(struct packet_reader *reader) {
 		}
 	}
 
-	if (server_version != 1 || die_version)
+	if (server_version != 1)
 		die("bad protocol version: %d", server_version);
 
+	if (die_write_version)
+		die("die with the --die-write-version option");
+
 	packet_write_fmt(1, "version=%d%c%s\n",
 			 version, '\0',
 			 use_push_options && !no_push_options ? "push-options": "");
@@ -75,11 +84,13 @@ static void proc_receive_read_commands(struct packet_reader *reader,
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 
+		if (die_read_commands)
+			die("die with the --die-read-commands option");
+
 		if (parse_oid_hex(reader->line, &old_oid, &p) ||
 		    *p++ != ' ' ||
 		    parse_oid_hex(p, &new_oid, &p) ||
-		    *p++ != ' ' ||
-		    die_readline)
+		    *p++ != ' ')
 			die("protocol error: expected 'old new ref', got '%s'",
 			    reader->line);
 		refname = p;
@@ -99,6 +110,9 @@ static void proc_receive_read_push_options(struct packet_reader *reader,
 	if (no_push_options || !use_push_options)
 	       return;
 
+	if (die_read_push_options)
+		die("die with the --die-read-push-options option");
+
 	while (1) {
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
@@ -117,10 +131,16 @@ int cmd__proc_receive(int argc, const char **argv)
 	struct option options[] = {
 		OPT_BOOL(0, "no-push-options", &no_push_options,
 			 "disable push options"),
-		OPT_BOOL(0, "die-version", &die_version,
-			 "die during version negotiation"),
-		OPT_BOOL(0, "die-readline", &die_readline,
-			 "die when readline"),
+		OPT_BOOL(0, "die-read-version", &die_read_version,
+			 "die when reading version"),
+		OPT_BOOL(0, "die-write-version", &die_write_version,
+			 "die when writing version"),
+		OPT_BOOL(0, "die-read-commands", &die_read_commands,
+			 "die when reading commands"),
+		OPT_BOOL(0, "die-read-push-options", &die_read_push_options,
+			 "die when reading push-options"),
+		OPT_BOOL(0, "die-write-report", &die_write_report,
+			 "die when writing report"),
 		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
 				"return of results"),
 		OPT__VERBOSE(&verbose, "be verbose"),
@@ -136,7 +156,7 @@ int cmd__proc_receive(int argc, const char **argv)
 		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
 	packet_reader_init(&reader, 0, NULL, 0,
 			   PACKET_READ_CHOMP_NEWLINE |
-			   PACKET_READ_DIE_ON_ERR_PACKET);
+			   PACKET_READ_GENTLE_ON_EOF);
 
 	sigchain_push(SIGPIPE, SIG_IGN);
 	proc_receive_verison(&reader);
@@ -166,6 +186,8 @@ int cmd__proc_receive(int argc, const char **argv)
 				fprintf(stderr, "proc-receive> %s\n", item->string);
 	}
 
+	if (die_write_report)
+		die("die with the --die-write-report option");
 	if (returns.nr)
 		for_each_string_list_item(item, &returns)
 			packet_write_fmt(1, "%s\n", item->string);
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index 854c3e884a..550ceaaf4b 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -16,7 +16,8 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 
 	# Check status report for git-push
 	sed -n \
-		-e "/^To / { p; n; p; }" \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
 		<actual >actual-report &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
@@ -41,32 +42,98 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 	test_cmp expect actual
 '
 
-test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
-	test-tool proc-receive -v --die-version
+	test-tool proc-receive -v --die-read-version
 	EOF
 '
 
 # Refs of upstream : main(A)
 # Refs of workbench: main(A)  tags/v123
 # git push         :                       refs/for/main/topic(A)
-test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-version
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
 
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-commands
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
 	cat >expect <<-EOF &&
-	remote: # pre-receive hook
-	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
-	remote: # proc-receive hook
-	remote: fatal: bad protocol version: 1
-	remote: error: proc-receive version "0" is not supported
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-commands option" out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -76,23 +143,65 @@ test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)"
 	test_cmp expect actual
 '
 
-test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
-	test-tool proc-receive -v --die-readline
+	test-tool proc-receive -v --die-read-push-options
 	EOF
 '
 
 # Refs of upstream : main(A)
 # Refs of workbench: main(A)  tags/v123
 # git push         :                       refs/for/main/topic(A)
-test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true &&
 	test_must_fail git -C workbench push origin \
+		-o reviewers=user1,user2 \
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-push-options option" out &&
+
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
 
-	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\"" actual &&
+test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-report
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-report option" out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -123,13 +232,15 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
 	remote: # proc-receive hook
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	remote: error: no response from proc-receive hook
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
 	To <URL/of/upstream.git>
 	 * [new branch] HEAD -> next
-	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -173,6 +284,7 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -208,6 +320,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index 88c56311da..5aeebe7553 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -42,6 +42,175 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porc
 	test_cmp expect actual
 '
 
+test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-version
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-version
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-commands
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-commands option" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-push-options
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions true &&
+	test_must_fail git -C workbench push --porcelain origin \
+		-o reviewers=user1,user2 \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-push-options option" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-report
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output -n \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-report option" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -63,14 +232,16 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
 	remote: # proc-receive hook
 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	remote: error: no response from proc-receive hook
 	remote: # post-receive hook
 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
 	To <URL/of/upstream.git>
 	*    HEAD:refs/heads/next    [new branch]
-	!    HEAD:refs/for/main/topic    [remote rejected] (proc-receive failed to report status)
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -84,7 +255,6 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 # Refs of workbench: main(A)  tags/v123
 test_expect_success "cleanup ($PROTOCOL/porcelain)" '
 	git -C "$upstream" update-ref -d refs/heads/next
-
 '
 
 test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
@@ -115,6 +285,7 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -151,6 +322,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porce
 	Done
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output
  2020-11-10 12:01                                                                   ` [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output Jiang Xin
@ 2020-11-10 20:51                                                                     ` Junio C Hamano
  2020-11-11 11:08                                                                       ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-11-10 20:51 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, Johannes Schindelin, Jeff King, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
> index 521a347710..b7cca2d8fb 100644
> --- a/t/t5411/common-functions.sh
> +++ b/t/t5411/common-functions.sh
> @@ -42,7 +42,7 @@ create_commits_in () {
>  make_user_friendly_and_stable_output () {
>  	sed \
>  		-e "s/  *\$//" \
> -		-e "s/   */ /g" \
> +		-e "s/  */ /g" \
>  		-e "s/'/\"/g" \
>  		-e "s/	/    /g" \
>  		-e "s/$A/<COMMIT-A>/g" \
> @@ -52,5 +52,11 @@ make_user_friendly_and_stable_output () {
>  		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
>  		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
>  		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
> -		-e "/^error: / d"
> +		-e "/^error: / d" | \
> +	if test $# -eq 0
> +	then
> +		cat
> +	else
> +		sed ${1+"$@"}
> +	fi
>  }

I may have suggested it, but looking at this implementation I'd have
to say it is not worth the extra process here.  The only reason why
I made the suggestion was that way we can make the single "sed"
invocation to do what we want.

If you need custom output for just two tests in 5411-0000, define
the custom one that wraps make_user_friendly_and_stable_output in
that single script like so:

    filter_out_remote_messages () {
	make_user_friendly_and_stable_output |
	sed -n -e ...
    }

and then use that ...


> diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
> index 2b04b49367..b3af3f59b0 100644
> --- a/t/t5411/test-0000-standard-git-push.sh
> +++ b/t/t5411/test-0000-standard-git-push.sh
> @@ -36,11 +36,10 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
>  		main \
>  		$B:refs/heads/next \
>  		>out 2>&1 &&
> -	make_user_friendly_and_stable_output <out |
> -		sed -n \
> -			-e "/^To / { s/   */ /g; p; }" \
> -			-e "/^ ! / { s/   */ /g; p; }" \
> -			>actual &&
> +	make_user_friendly_and_stable_output -n \
> +		-e "/^To / { p; }" \
> +		-e "/^ ! / { p; }" \
> +		<out >actual &&

... perhaps like

	filter_out_remote_messages <out >actual &&	

here?

> diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
> index 747307f8da..16ff2d5666 100644
> --- a/t/t5411/test-0001-standard-git-push--porcelain.sh
> +++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
> @@ -37,16 +37,15 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
>  		main \
>  		$B:refs/heads/next \
>  		>out 2>&1 &&
> -	make_user_friendly_and_stable_output <out |
> -		sed -n \
> -			-e "s/^# GETTEXT POISON #//" \
> -			-e "/^To / { s/   */ /g; p; }" \
> -			-e "/^! / { s/   */ /g; p; }" \
> -			>actual &&
> +	make_user_friendly_and_stable_output -n \
> +		-e "s/^# GETTEXT POISON #//" \
> +		-e "/^To / { p; }" \
> +		-e "/^! / { p; }" \
> +		<out >actual &&
>  	cat >expect <<-EOF &&
>  	To <URL/of/upstream.git>
> -	! refs/heads/main:refs/heads/main [rejected] (non-fast-forward)
> -	! <COMMIT-B>:refs/heads/next [rejected] (atomic push failed)
> +	!    refs/heads/main:refs/heads/main    [rejected] (non-fast-forward)
> +	!    <COMMIT-B>:refs/heads/next    [rejected] (atomic push failed)
>  	EOF
>  	test_cmp expect actual &&
>  	git -C "$upstream" show-ref >out &&

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-10 11:49                                                                 ` Jiang Xin
  2020-11-10 12:01                                                                   ` [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output Jiang Xin
  2020-11-10 12:01                                                                   ` [PATCH v3 2/2] receive-pack: gently write messages to proc-receive Jiang Xin
@ 2020-11-10 21:00                                                                   ` Junio C Hamano
  2020-11-10 21:13                                                                     ` Junio C Hamano
  2020-11-11 11:31                                                                   ` [PATCH v4 0/3] jx/t5411-flake-fix Jiang Xin
                                                                                     ` (3 subsequent siblings)
  6 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2020-11-10 21:00 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Jeff King, Git List, Johannes Schindelin, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> Thanks Peff for pointing the root cause. Will use the "gently" forms
> of packet_write_fmt() and packet_flush() in patch v3.

Another thing I noticed.  Its tests are based on a codebase where
master is called main, which means it is not designed to be usable
with the 'master' or 'maint' track.  

Because the topic is no longer "just apply a band-aid to paper over
test flakeyness" but is fixing the codepath that matters in real
user experience, I'd prefer to see it applicable even to the
maintenance track.

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2] t5411: consistent result for proc-receive broken test
  2020-11-10 21:00                                                                   ` [PATCH v2] t5411: consistent result for proc-receive broken test Junio C Hamano
@ 2020-11-10 21:13                                                                     ` Junio C Hamano
  0 siblings, 0 replies; 266+ messages in thread
From: Junio C Hamano @ 2020-11-10 21:13 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Jeff King, Git List, Johannes Schindelin, Jiang Xin

Junio C Hamano <gitster@pobox.com> writes:

> Jiang Xin <worldhello.net@gmail.com> writes:
>
>> Thanks Peff for pointing the root cause. Will use the "gently" forms
>> of packet_write_fmt() and packet_flush() in patch v3.
>
> Another thing I noticed.  Its tests are based on a codebase where
> master is called main, which means it is not designed to be usable
> with the 'master' or 'maint' track.  
>
> Because the topic is no longer "just apply a band-aid to paper over
> test flakeyness" but is fixing the codepath that matters in real
> user experience, I'd prefer to see it applicable even to the
> maintenance track.

I've queued these patches on top of
js/default-branch-name-adjust-t5411; that makes the topic mergeable
to 'master' but not to 'maint'.  Perhaps that is good enough, as the
maintenance track of today would become obsolete by the end of this
year.

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v3 2/2] receive-pack: gently write messages to proc-receive
  2020-11-10 12:01                                                                   ` [PATCH v3 2/2] receive-pack: gently write messages to proc-receive Jiang Xin
@ 2020-11-10 21:52                                                                     ` Jeff King
  2020-11-11 11:03                                                                       ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Jeff King @ 2020-11-10 21:52 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Git List, Johannes Schindelin, Jiang Xin

On Tue, Nov 10, 2020 at 08:01:35PM +0800, Jiang Xin wrote:

> Johannes found a flaky hang in `t5411/test-0013-bad-protocol.sh` in the
> osx-clang job of the CI/PR builds, and ran into an issue when using
> the `--stress` option with the following error messages:
> 
>     fatal: unable to write flush packet: Broken pipe
>     send-pack: unexpected disconnect while reading sideband packet
>     fatal: the remote end hung up unexpectedly
> 
> In this test case, the "proc-receive" hook sends an error message and
> dies earlier. While "receive-pack" on the other side of the pipe
> should forward the error message of the "proc-receive" hook to the
> client side, but it fails to do so. This is because "receive-pack"
> uses `packet_write_fmt()` and `packet_flush()` to write pkt-line
> message to "proc-receive" hook, and these functions die immediately
> when pipe is broken. Using "gently" forms for these functions will get
> more predicable output.

The changes to use gently() in the code looked good to me, and I think
you got all of the relevant spots.

I was surprised by a few bits:

> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> index bb9909c52e..697a4e8802 100644
> --- a/builtin/receive-pack.c
> +++ b/builtin/receive-pack.c
> @@ -974,9 +974,10 @@ static int read_proc_receive_report(struct packet_reader *reader,
>  	struct command *cmd;
>  	struct command *hint = NULL;
>  	struct ref_push_report *report = NULL;
> -	int new_report = 0;
>  	int code = 0;
> +	int new_report = 0;

This is just noise in the diff, I think. It does not matter either way.

> @@ -984,8 +985,14 @@ static int read_proc_receive_report(struct packet_reader *reader,
>  		const char *refname;
>  		char *p;
>  
> -		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
> +		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
> +			if (!response) {
> +				strbuf_addstr(errmsg, "no response from proc-receive hook");
> +				return -1;
> +			}
>  			break;
> +		}
> +		response++;

This extra check seems orthogonal to the rest of the commit. It does
seem like it might be a reasonable thing to check, but I wondered:

  - if the hook has nothing to report, wouldn't it just send a flush
    packet? Does that break protocol or not?

  - if not, then I guess we're expecting a response for every ref we
    mentioned (and presumably we would not trigger the hook at all if
    there are no refs). But in that case, shouldn't we be checking that
    we counted up the number of responses we expected? But we already do
    that, by annotating the items in the commands list that didn't get
    RUN_PROC_RECEIVE_RETURNED set.

So at best, it seems like this check is redundant (and at worst it may
complain unnecessarily about a corner case).

> @@ -1100,7 +1107,7 @@ static int run_proc_receive_hook(struct command *commands,
>  	struct strbuf cap = STRBUF_INIT;
>  	struct strbuf errmsg = STRBUF_INIT;
>  	int hook_use_push_options = 0;
> -	int version = 0;
> +	int version = -1;
> [...]
> -	if (version != 1) {
> +	if (version == -1) {
> +		strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook");
> +		code = -1;
> +		goto cleanup;
> +	} else if (version != 1) {
>  		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
>  			    version);

Likewise this seems orthogonal to the main point of the patch. Though it
seems like a good idea in general to check when the other side doesn't
report a version (assuming it is a protocol breakage not to report the
version, and we're not simply supposed to default).

-Peff

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v3 2/2] receive-pack: gently write messages to proc-receive
  2020-11-10 21:52                                                                     ` Jeff King
@ 2020-11-11 11:03                                                                       ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-11 11:03 UTC (permalink / raw)
  To: Jeff King; +Cc: Junio C Hamano, Git List, Johannes Schindelin, Jiang Xin

Jeff King <peff@peff.net> 于2020年11月11日周三 上午5:52写道:
> > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> > index bb9909c52e..697a4e8802 100644
> > --- a/builtin/receive-pack.c
> > +++ b/builtin/receive-pack.c
> > @@ -974,9 +974,10 @@ static int read_proc_receive_report(struct packet_reader *reader,
> >       struct command *cmd;
> >       struct command *hint = NULL;
> >       struct ref_push_report *report = NULL;
> > -     int new_report = 0;
> >       int code = 0;
> > +     int new_report = 0;
>
> This is just noise in the diff, I think. It does not matter either way.
>
> > @@ -984,8 +985,14 @@ static int read_proc_receive_report(struct packet_reader *reader,
> >               const char *refname;
> >               char *p;
> >
> > -             if (packet_reader_read(reader) != PACKET_READ_NORMAL)
> > +             if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
> > +                     if (!response) {
> > +                             strbuf_addstr(errmsg, "no response from proc-receive hook");
> > +                             return -1;
> > +                     }
> >                       break;
> > +             }
> > +             response++;
>
> This extra check seems orthogonal to the rest of the commit. It does
> seem like it might be a reasonable thing to check, but I wondered:
>
>   - if the hook has nothing to report, wouldn't it just send a flush
>     packet? Does that break protocol or not?

It's OK to send a flush packet without any payload, so will check
status PACKET_READ_EOF for broken proc-receive.

-- snip --
@@ -977,15 +977,25 @@ static int read_proc_receive_report(struct
packet_reader *reader,
        for (;;) {
                struct object_id old_oid, new_oid;
                const char *head;
                const char *refname;
                char *p;
-
-               if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+               enum packet_read_status status;
+
+               status = packet_reader_read(reader);
+               if (status != PACKET_READ_NORMAL) {
+                       /* Check whether proc-receive exited abnormally */
+                       if (status == PACKET_READ_EOF && !response) {
+                               strbuf_addstr(errmsg, "proc-receive
exited abnormally");
+                               return -1;
+                       }
                        break;
+               }
+               response++;
-- snap --

>
> > @@ -1100,7 +1107,7 @@ static int run_proc_receive_hook(struct command *commands,
> >       struct strbuf cap = STRBUF_INIT;
> >       struct strbuf errmsg = STRBUF_INIT;
> >       int hook_use_push_options = 0;
> > -     int version = 0;
> > +     int version = -1;
> > [...]
> > -     if (version != 1) {
> > +     if (version == -1) {
> > +             strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook");
> > +             code = -1;
> > +             goto cleanup;
> > +     } else if (version != 1) {
> >               strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
> >                           version);
>
> Likewise this seems orthogonal to the main point of the patch. Though it
> seems like a good idea in general to check when the other side doesn't
> report a version (assuming it is a protocol breakage not to report the
> version, and we're not simply supposed to default).

Will add a new patch for default version for proc-receive.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output
  2020-11-10 20:51                                                                     ` Junio C Hamano
@ 2020-11-11 11:08                                                                       ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-11 11:08 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, Johannes Schindelin, Jeff King, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2020年11月11日周三 上午4:51写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
> > index 521a347710..b7cca2d8fb 100644
> > --- a/t/t5411/common-functions.sh
> > +++ b/t/t5411/common-functions.sh
> > @@ -42,7 +42,7 @@ create_commits_in () {
> >  make_user_friendly_and_stable_output () {
> >       sed \
> >               -e "s/  *\$//" \
> > -             -e "s/   */ /g" \
> > +             -e "s/  */ /g" \
> >               -e "s/'/\"/g" \
> >               -e "s/  /    /g" \
> >               -e "s/$A/<COMMIT-A>/g" \
> > @@ -52,5 +52,11 @@ make_user_friendly_and_stable_output () {
> >               -e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
> >               -e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
> >               -e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
> > -             -e "/^error: / d"
> > +             -e "/^error: / d" | \
> > +     if test $# -eq 0
> > +     then
> > +             cat
> > +     else
> > +             sed ${1+"$@"}
> > +     fi
> >  }
>
> I may have suggested it, but looking at this implementation I'd have
> to say it is not worth the extra process here.  The only reason why
> I made the suggestion was that way we can make the single "sed"
> invocation to do what we want.
>
> If you need custom output for just two tests in 5411-0000, define
> the custom one that wraps make_user_friendly_and_stable_output in
> that single script like so:
>
>     filter_out_remote_messages () {
>         make_user_friendly_and_stable_output |
>         sed -n -e ...
>     }
>
> and then use that ...

Will add new helper function
`filter_out_user_friendly_and_stable_output` like this, and use it for
5411-0000, 5411-0001, 5411-0013 and 5411-0014.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v4 0/3] jx/t5411-flake-fix
  2020-11-10 11:49                                                                 ` Jiang Xin
                                                                                     ` (2 preceding siblings ...)
  2020-11-10 21:00                                                                   ` [PATCH v2] t5411: consistent result for proc-receive broken test Junio C Hamano
@ 2020-11-11 11:31                                                                   ` Jiang Xin
  2020-11-11 11:32                                                                   ` [PATCH v4 1/3] t5411: new helper filter_out_user_friendly_and_stable_output Jiang Xin
                                                                                     ` (2 subsequent siblings)
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-11 11:31 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin, Jeff King; +Cc: Jiang Xin

The exchange between receive-pack and proc-receive hook did not
carefully check for errors.

## Changes since v3

1. Patch 1/3:

   Add new helper function `filter_out_user_friendly_and_stable_output`,
   and use it for 5411-0000, 5411-0001, 5411-0013 and 5411-0014.

2. Patch 2/3:

   Check PACKET_READ_EOF status from reader for broken pipe.

3. Patch 3/3:

   Add a new patch, which uses version 0 for proc-receive by default.


## Range-diff of v3...v4

1:  785b28333f ! 1:  a3bdb5a5d6 t5411: refactor make_user_friendly_and_stable_output
    @@ Metadata
     Author: Jiang Xin <worldhello.net@gmail.com>
     
      ## Commit message ##
    -    t5411: refactor make_user_friendly_and_stable_output
    +    t5411: new helper filter_out_user_friendly_and_stable_output
     
    -    Common function `make_user_friendly_and_stable_output()` can take
    -    additional arguments to format messages for special cases.
    +    New helper `filter_out_user_friendly_and_stable_output` will call
    +    common helpr function `make_user_friendly_and_stable_output` and use
    +    additional arguments to filter out messages for specific test cases.
     
         Suggested-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## t/t5411/common-functions.sh ##
     @@ t/t5411/common-functions.sh: create_commits_in () {
    @@ t/t5411/common-functions.sh: create_commits_in () {
      		-e "s/	/    /g" \
      		-e "s/$A/<COMMIT-A>/g" \
     @@ t/t5411/common-functions.sh: make_user_friendly_and_stable_output () {
    - 		-e "s/$(echo $A | cut -c1-7)[0-9a-f]*/<OID-A>/g" \
    - 		-e "s/$(echo $B | cut -c1-7)[0-9a-f]*/<OID-B>/g" \
      		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
    --		-e "/^error: / d"
    -+		-e "/^error: / d" | \
    -+	if test $# -eq 0
    -+	then
    -+		cat
    -+	else
    -+		sed ${1+"$@"}
    -+	fi
    + 		-e "/^error: / d"
      }
    ++
    ++filter_out_user_friendly_and_stable_output () {
    ++	make_user_friendly_and_stable_output |
    ++		sed -n ${1+"$@"}
    ++}
     
      ## t/t5411/test-0000-standard-git-push.sh ##
     @@ t/t5411/test-0000-standard-git-push.sh: test_expect_success "git-push --atomic ($PROTOCOL)" '
    @@ t/t5411/test-0000-standard-git-push.sh: test_expect_success "git-push --atomic (
     -			-e "/^To / { s/   */ /g; p; }" \
     -			-e "/^ ! / { s/   */ /g; p; }" \
     -			>actual &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; }" \
     +		-e "/^ ! / { p; }" \
     +		<out >actual &&
    @@ t/t5411/test-0001-standard-git-push--porcelain.sh: test_expect_success "git-push
     -			-e "/^To / { s/   */ /g; p; }" \
     -			-e "/^! / { s/   */ /g; p; }" \
     -			>actual &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "s/^# GETTEXT POISON #//" \
     +		-e "/^To / { p; }" \
     +		-e "/^! / { p; }" \
2:  d83d4519db ! 2:  4ca1c6e2bc receive-pack: gently write messages to proc-receive
    @@ Commit message
         Reported-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
         Suggested-by: Jeff King <peff@peff.net>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## builtin/receive-pack.c ##
     @@ builtin/receive-pack.c: static int read_proc_receive_report(struct packet_reader *reader,
    - 	struct command *cmd;
    - 	struct command *hint = NULL;
    - 	struct ref_push_report *report = NULL;
    --	int new_report = 0;
    + 	int new_report = 0;
      	int code = 0;
    -+	int new_report = 0;
      	int once = 0;
     +	int response = 0;
      
      	for (;;) {
      		struct object_id old_oid, new_oid;
    -@@ builtin/receive-pack.c: static int read_proc_receive_report(struct packet_reader *reader,
    + 		const char *head;
      		const char *refname;
      		char *p;
    - 
    +-
     -		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
    -+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
    -+			if (!response) {
    -+				strbuf_addstr(errmsg, "no response from proc-receive hook");
    ++		enum packet_read_status status;
    ++
    ++		status = packet_reader_read(reader);
    ++		if (status != PACKET_READ_NORMAL) {
    ++			/* Check whether proc-receive exited abnormally */
    ++			if (status == PACKET_READ_EOF && !response) {
    ++				strbuf_addstr(errmsg, "proc-receive exited abnormally");
     +				return -1;
     +			}
      			break;
    @@ builtin/receive-pack.c: static int read_proc_receive_report(struct packet_reader
      
      		head = reader->line;
      		p = strchr(head, ' ');
    -@@ builtin/receive-pack.c: static int run_proc_receive_hook(struct command *commands,
    - 	struct strbuf cap = STRBUF_INIT;
    - 	struct strbuf errmsg = STRBUF_INIT;
    - 	int hook_use_push_options = 0;
    --	int version = 0;
    -+	int version = -1;
    - 	int code;
    - 
    - 	argv[0] = find_hook("proc-receive");
     @@ builtin/receive-pack.c: static int run_proc_receive_hook(struct command *commands,
      	if (use_push_options)
      		strbuf_addstr(&cap, " push-options");
    @@ builtin/receive-pack.c: static int run_proc_receive_hook(struct command *command
      	} else {
     -		packet_write_fmt(proc.in, "version=1\n");
     +		code = packet_write_fmt_gently(proc.in, "version=1\n");
    -+	}
    + 	}
    +-	packet_flush(proc.in);
     +	if (!code)
     +		code = packet_flush_gently(proc.in);
    + 
    +-	for (;;) {
    +-		int linelen;
    ++	if (!code)
    ++		for (;;) {
    ++			int linelen;
    ++			enum packet_read_status status;
    + 
    +-		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
    +-			break;
    ++			status = packet_reader_read(&reader);
    ++			if (status != PACKET_READ_NORMAL) {
    ++				/* Check whether proc-receive exited abnormally */
    ++				if (status == PACKET_READ_EOF)
    ++					code = -1;
    ++				break;
    ++			}
    + 
    +-		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
    +-			version = atoi(reader.line + 8);
    +-			linelen = strlen(reader.line);
    +-			if (linelen < reader.pktlen) {
    +-				const char *feature_list = reader.line + linelen + 1;
    +-				if (parse_feature_request(feature_list, "push-options"))
    +-					hook_use_push_options = 1;
    ++			if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
    ++				version = atoi(reader.line + 8);
    ++				linelen = strlen(reader.line);
    ++				if (linelen < reader.pktlen) {
    ++					const char *feature_list = reader.line + linelen + 1;
    ++					if (parse_feature_request(feature_list, "push-options"))
    ++						hook_use_push_options = 1;
    ++				}
    + 			}
    + 		}
    ++
     +	if (code) {
     +		strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook");
     +		goto cleanup;
      	}
    --	packet_flush(proc.in);
      
    - 	for (;;) {
    - 		int linelen;
    -@@ builtin/receive-pack.c: static int run_proc_receive_hook(struct command *commands,
    - 		}
    - 	}
    - 
    --	if (version != 1) {
    -+	if (version == -1) {
    -+		strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook");
    -+		code = -1;
    -+		goto cleanup;
    -+	} else if (version != 1) {
    - 		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
    - 			    version);
    - 		code = -1;
    + 	if (version != 1) {
     @@ builtin/receive-pack.c: static int run_proc_receive_hook(struct command *commands,
      	for (cmd = commands; cmd; cmd = cmd->next) {
      		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
      	test_must_fail git -C workbench push origin \
      		HEAD:refs/for/main/topic \
      		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; }" \
     +		-e "/^ ! / { p; }" \
     +		<out >actual &&
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
     +	test-tool proc-receive -v --die-write-version
     +	EOF
     +'
    -+
    + 
     +# Refs of upstream : main(A)
     +# Refs of workbench: main(A)  tags/v123
     +# git push         :                       refs/for/main/topic(A)
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; }" \
     +		-e "/^ ! / { p; }" \
     +		<out >actual &&
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
     +	test_cmp expect actual &&
     +	grep "remote: fatal: die with the --die-write-version option" out &&
     +	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
    - 
    ++
     +	git -C "$upstream" show-ref >out &&
     +	make_user_friendly_and_stable_output <out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; }" \
     +		-e "/^ ! / { p; }" \
     +		<out >actual &&
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
     +		-o reviewers=user1,user2 \
      		HEAD:refs/for/main/topic \
      		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; }" \
     +		-e "/^ ! / { p; }" \
     +		<out >actual &&
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
     +	EOF
     +	test_cmp expect actual
     +'
    - 
    --	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\"" actual &&
    ++
     +test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL)" '
     +	write_script "$upstream/hooks/proc-receive" <<-EOF
     +	printf >&2 "# proc-receive hook\n"
     +	test-tool proc-receive -v --die-write-report
     +	EOF
     +'
    -+
    + 
    +-	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\"" actual &&
     +# Refs of upstream : main(A)
     +# Refs of workbench: main(A)  tags/v123
     +# git push         :                       refs/for/main/topic(A)
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
     +	test_must_fail git -C workbench push origin \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; }" \
     +		-e "/^ ! / { p; }" \
     +		<out >actual &&
    @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protoc
      	git -C "$upstream" show-ref >out &&
      	make_user_friendly_and_stable_output <out >actual &&
     @@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
    - 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
    - 	remote: # proc-receive hook
    - 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
    -+	remote: error: no response from proc-receive hook
    - 	remote: # post-receive hook
    - 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    - 	To <URL/of/upstream.git>
    - 	 * [new branch] HEAD -> next
    --	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
    -+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
    + 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
      	EOF
      	test_cmp expect actual &&
     +
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive:
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; n; p; n; p; }" \
     +		<out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive:
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; n; p; n; p; }" \
     +		<out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive:
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; n; p; n; p; }" \
     +		<out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive:
     +		-o reviewers=user1,user2 \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; n; p; n; p; }" \
     +		<out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive:
     +	test_must_fail git -C workbench push --porcelain origin \
     +		HEAD:refs/for/main/topic \
     +		>out 2>&1 &&
    -+	make_user_friendly_and_stable_output -n \
    ++	filter_out_user_friendly_and_stable_output \
     +		-e "/^To / { p; n; p; n; p; }" \
     +		<out >actual &&
     +	cat >expect <<-EOF &&
    @@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive:
      	write_script "$upstream/hooks/proc-receive" <<-EOF
      	printf >&2 "# proc-receive hook\n"
     @@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
    - 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
    - 	remote: # proc-receive hook
    - 	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
    -+	remote: error: no response from proc-receive hook
    - 	remote: # post-receive hook
    - 	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
    - 	To <URL/of/upstream.git>
    - 	*    HEAD:refs/heads/next    [new branch]
    --	!    HEAD:refs/for/main/topic    [remote rejected] (proc-receive failed to report status)
    -+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
      	Done
      	EOF
      	test_cmp expect actual &&
-:  ---------- > 3:  82d03e6508 receive-pack: use default version 0 for proc-receive

---

Jiang Xin (3):
  t5411: new helper filter_out_user_friendly_and_stable_output
  receive-pack: gently write messages to proc-receive
  receive-pack: use default version 0 for proc-receive

 builtin/receive-pack.c                        |  94 +++++++---
 t/helper/test-proc-receive.c                  |  54 ++++--
 t/t5411/common-functions.sh                   |   7 +-
 t/t5411/test-0000-standard-git-push.sh        |   9 +-
 .../test-0001-standard-git-push--porcelain.sh |  15 +-
 t/t5411/test-0013-bad-protocol.sh             | 138 ++++++++++++--
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 173 +++++++++++++++++-
 t/t5411/test-0026-push-options.sh             |  60 ++++++
 t/t5411/test-0027-push-options--porcelain.sh  |  62 +++++++
 9 files changed, 545 insertions(+), 67 deletions(-)

-- 
2.29.0.dirty


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v4 1/3] t5411: new helper filter_out_user_friendly_and_stable_output
  2020-11-10 11:49                                                                 ` Jiang Xin
                                                                                     ` (3 preceding siblings ...)
  2020-11-11 11:31                                                                   ` [PATCH v4 0/3] jx/t5411-flake-fix Jiang Xin
@ 2020-11-11 11:32                                                                   ` Jiang Xin
  2020-11-11 11:32                                                                   ` [PATCH v4 2/3] receive-pack: gently write messages to proc-receive Jiang Xin
  2020-11-11 11:32                                                                   ` [PATCH v4 3/3] receive-pack: use default version 0 for proc-receive Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-11 11:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin, Jeff King
  Cc: Jiang Xin, Jiang Xin

From: Jiang Xin <worldhello.net@gmail.com>

New helper `filter_out_user_friendly_and_stable_output` will call
common helpr function `make_user_friendly_and_stable_output` and use
additional arguments to filter out messages for specific test cases.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/common-functions.sh                       |  7 ++++++-
 t/t5411/test-0000-standard-git-push.sh            |  9 ++++-----
 t/t5411/test-0001-standard-git-push--porcelain.sh | 15 +++++++--------
 3 files changed, 17 insertions(+), 14 deletions(-)

diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 521a347710..344d13f61a 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -42,7 +42,7 @@ create_commits_in () {
 make_user_friendly_and_stable_output () {
 	sed \
 		-e "s/  *\$//" \
-		-e "s/   */ /g" \
+		-e "s/  */ /g" \
 		-e "s/'/\"/g" \
 		-e "s/	/    /g" \
 		-e "s/$A/<COMMIT-A>/g" \
@@ -54,3 +54,8 @@ make_user_friendly_and_stable_output () {
 		-e "s#To $URL_PREFIX/upstream.git#To <URL/of/upstream.git>#" \
 		-e "/^error: / d"
 }
+
+filter_out_user_friendly_and_stable_output () {
+	make_user_friendly_and_stable_output |
+		sed -n ${1+"$@"}
+}
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index 2b04b49367..47b058af7e 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -36,11 +36,10 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 		main \
 		$B:refs/heads/next \
 		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out |
-		sed -n \
-			-e "/^To / { s/   */ /g; p; }" \
-			-e "/^ ! / { s/   */ /g; p; }" \
-			>actual &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [rejected] main -> main (non-fast-forward)
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index 747307f8da..bbead12bbb 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -37,16 +37,15 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 		main \
 		$B:refs/heads/next \
 		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out |
-		sed -n \
-			-e "s/^# GETTEXT POISON #//" \
-			-e "/^To / { s/   */ /g; p; }" \
-			-e "/^! / { s/   */ /g; p; }" \
-			>actual &&
+	filter_out_user_friendly_and_stable_output \
+		-e "s/^# GETTEXT POISON #//" \
+		-e "/^To / { p; }" \
+		-e "/^! / { p; }" \
+		<out >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
-	! refs/heads/main:refs/heads/main [rejected] (non-fast-forward)
-	! <COMMIT-B>:refs/heads/next [rejected] (atomic push failed)
+	!    refs/heads/main:refs/heads/main    [rejected] (non-fast-forward)
+	!    <COMMIT-B>:refs/heads/next    [rejected] (atomic push failed)
 	EOF
 	test_cmp expect actual &&
 	git -C "$upstream" show-ref >out &&
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 2/3] receive-pack: gently write messages to proc-receive
  2020-11-10 11:49                                                                 ` Jiang Xin
                                                                                     ` (4 preceding siblings ...)
  2020-11-11 11:32                                                                   ` [PATCH v4 1/3] t5411: new helper filter_out_user_friendly_and_stable_output Jiang Xin
@ 2020-11-11 11:32                                                                   ` Jiang Xin
  2020-11-11 11:32                                                                   ` [PATCH v4 3/3] receive-pack: use default version 0 for proc-receive Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-11 11:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin, Jeff King
  Cc: Jiang Xin, Jiang Xin

From: Jiang Xin <worldhello.net@gmail.com>

Johannes found a flaky hang in `t5411/test-0013-bad-protocol.sh` in the
osx-clang job of the CI/PR builds, and ran into an issue when using
the `--stress` option with the following error messages:

    fatal: unable to write flush packet: Broken pipe
    send-pack: unexpected disconnect while reading sideband packet
    fatal: the remote end hung up unexpectedly

In this test case, the "proc-receive" hook sends an error message and
dies earlier. While "receive-pack" on the other side of the pipe
should forward the error message of the "proc-receive" hook to the
client side, but it fails to do so. This is because "receive-pack"
uses `packet_write_fmt()` and `packet_flush()` to write pkt-line
message to "proc-receive" hook, and these functions die immediately
when pipe is broken. Using "gently" forms for these functions will get
more predicable output.

Add more "--die-*" options to test helper to test different stages of
the protocol between "receive-pack" and "proc-receive" hook.

Reported-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                       |  87 +++++++---
 t/helper/test-proc-receive.c                 |  42 +++--
 t/t5411/test-0013-bad-protocol.sh            | 138 +++++++++++++--
 t/t5411/test-0014-bad-protocol--porcelain.sh | 173 ++++++++++++++++++-
 4 files changed, 392 insertions(+), 48 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index bb9909c52e..2bd736525f 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -977,15 +977,25 @@ static int read_proc_receive_report(struct packet_reader *reader,
 	int new_report = 0;
 	int code = 0;
 	int once = 0;
+	int response = 0;
 
 	for (;;) {
 		struct object_id old_oid, new_oid;
 		const char *head;
 		const char *refname;
 		char *p;
-
-		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
+		enum packet_read_status status;
+
+		status = packet_reader_read(reader);
+		if (status != PACKET_READ_NORMAL) {
+			/* Check whether proc-receive exited abnormally */
+			if (status == PACKET_READ_EOF && !response) {
+				strbuf_addstr(errmsg, "proc-receive exited abnormally");
+				return -1;
+			}
 			break;
+		}
+		response++;
 
 		head = reader->line;
 		p = strchr(head, ' ');
@@ -1145,28 +1155,41 @@ static int run_proc_receive_hook(struct command *commands,
 	if (use_push_options)
 		strbuf_addstr(&cap, " push-options");
 	if (cap.len) {
-		packet_write_fmt(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
+		code = packet_write_fmt_gently(proc.in, "version=1%c%s\n", '\0', cap.buf + 1);
 		strbuf_release(&cap);
 	} else {
-		packet_write_fmt(proc.in, "version=1\n");
+		code = packet_write_fmt_gently(proc.in, "version=1\n");
 	}
-	packet_flush(proc.in);
+	if (!code)
+		code = packet_flush_gently(proc.in);
 
-	for (;;) {
-		int linelen;
+	if (!code)
+		for (;;) {
+			int linelen;
+			enum packet_read_status status;
 
-		if (packet_reader_read(&reader) != PACKET_READ_NORMAL)
-			break;
+			status = packet_reader_read(&reader);
+			if (status != PACKET_READ_NORMAL) {
+				/* Check whether proc-receive exited abnormally */
+				if (status == PACKET_READ_EOF)
+					code = -1;
+				break;
+			}
 
-		if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
-			version = atoi(reader.line + 8);
-			linelen = strlen(reader.line);
-			if (linelen < reader.pktlen) {
-				const char *feature_list = reader.line + linelen + 1;
-				if (parse_feature_request(feature_list, "push-options"))
-					hook_use_push_options = 1;
+			if (reader.pktlen > 8 && starts_with(reader.line, "version=")) {
+				version = atoi(reader.line + 8);
+				linelen = strlen(reader.line);
+				if (linelen < reader.pktlen) {
+					const char *feature_list = reader.line + linelen + 1;
+					if (parse_feature_request(feature_list, "push-options"))
+						hook_use_push_options = 1;
+				}
 			}
 		}
+
+	if (code) {
+		strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook");
+		goto cleanup;
 	}
 
 	if (version != 1) {
@@ -1180,20 +1203,36 @@ static int run_proc_receive_hook(struct command *commands,
 	for (cmd = commands; cmd; cmd = cmd->next) {
 		if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string)
 			continue;
-		packet_write_fmt(proc.in, "%s %s %s",
-				 oid_to_hex(&cmd->old_oid),
-				 oid_to_hex(&cmd->new_oid),
-				 cmd->ref_name);
+		code = packet_write_fmt_gently(proc.in, "%s %s %s",
+					       oid_to_hex(&cmd->old_oid),
+					       oid_to_hex(&cmd->new_oid),
+					       cmd->ref_name);
+		if (code)
+			break;
+	}
+	if (!code)
+		code = packet_flush_gently(proc.in);
+	if (code) {
+		strbuf_addstr(&errmsg, "fail to write commands to proc-receive hook");
+		goto cleanup;
 	}
-	packet_flush(proc.in);
 
 	/* Send push options */
 	if (hook_use_push_options) {
 		struct string_list_item *item;
 
-		for_each_string_list_item(item, push_options)
-			packet_write_fmt(proc.in, "%s", item->string);
-		packet_flush(proc.in);
+		for_each_string_list_item(item, push_options) {
+			code = packet_write_fmt_gently(proc.in, "%s", item->string);
+			if (code)
+				break;
+		}
+		if (!code)
+			code = packet_flush_gently(proc.in);
+		if (code) {
+			strbuf_addstr(&errmsg,
+				      "fail to write push-options to proc-receive hook");
+			goto cleanup;
+		}
 	}
 
 	/* Read result from proc-receive */
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index 42164d9898..6652cedcee 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -10,8 +10,11 @@ static const char *proc_receive_usage[] = {
 	NULL
 };
 
-static int die_version;
-static int die_readline;
+static int die_read_version;
+static int die_write_version;
+static int die_read_commands;
+static int die_read_push_options;
+static int die_write_report;
 static int no_push_options;
 static int use_atomic;
 static int use_push_options;
@@ -33,6 +36,9 @@ struct command {
 static void proc_receive_verison(struct packet_reader *reader) {
 	int server_version = 0;
 
+	if (die_read_version)
+		die("die with the --die-read-version option");
+
 	for (;;) {
 		int linelen;
 
@@ -52,9 +58,12 @@ static void proc_receive_verison(struct packet_reader *reader) {
 		}
 	}
 
-	if (server_version != 1 || die_version)
+	if (server_version != 1)
 		die("bad protocol version: %d", server_version);
 
+	if (die_write_version)
+		die("die with the --die-write-version option");
+
 	packet_write_fmt(1, "version=%d%c%s\n",
 			 version, '\0',
 			 use_push_options && !no_push_options ? "push-options": "");
@@ -75,11 +84,13 @@ static void proc_receive_read_commands(struct packet_reader *reader,
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 
+		if (die_read_commands)
+			die("die with the --die-read-commands option");
+
 		if (parse_oid_hex(reader->line, &old_oid, &p) ||
 		    *p++ != ' ' ||
 		    parse_oid_hex(p, &new_oid, &p) ||
-		    *p++ != ' ' ||
-		    die_readline)
+		    *p++ != ' ')
 			die("protocol error: expected 'old new ref', got '%s'",
 			    reader->line);
 		refname = p;
@@ -99,6 +110,9 @@ static void proc_receive_read_push_options(struct packet_reader *reader,
 	if (no_push_options || !use_push_options)
 	       return;
 
+	if (die_read_push_options)
+		die("die with the --die-read-push-options option");
+
 	while (1) {
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
@@ -117,10 +131,16 @@ int cmd__proc_receive(int argc, const char **argv)
 	struct option options[] = {
 		OPT_BOOL(0, "no-push-options", &no_push_options,
 			 "disable push options"),
-		OPT_BOOL(0, "die-version", &die_version,
-			 "die during version negotiation"),
-		OPT_BOOL(0, "die-readline", &die_readline,
-			 "die when readline"),
+		OPT_BOOL(0, "die-read-version", &die_read_version,
+			 "die when reading version"),
+		OPT_BOOL(0, "die-write-version", &die_write_version,
+			 "die when writing version"),
+		OPT_BOOL(0, "die-read-commands", &die_read_commands,
+			 "die when reading commands"),
+		OPT_BOOL(0, "die-read-push-options", &die_read_push_options,
+			 "die when reading push-options"),
+		OPT_BOOL(0, "die-write-report", &die_write_report,
+			 "die when writing report"),
 		OPT_STRING_LIST('r', "return", &returns, "old/new/ref/status/msg",
 				"return of results"),
 		OPT__VERBOSE(&verbose, "be verbose"),
@@ -136,7 +156,7 @@ int cmd__proc_receive(int argc, const char **argv)
 		usage_msg_opt("Too many arguments.", proc_receive_usage, options);
 	packet_reader_init(&reader, 0, NULL, 0,
 			   PACKET_READ_CHOMP_NEWLINE |
-			   PACKET_READ_DIE_ON_ERR_PACKET);
+			   PACKET_READ_GENTLE_ON_EOF);
 
 	sigchain_push(SIGPIPE, SIG_IGN);
 	proc_receive_verison(&reader);
@@ -166,6 +186,8 @@ int cmd__proc_receive(int argc, const char **argv)
 				fprintf(stderr, "proc-receive> %s\n", item->string);
 	}
 
+	if (die_write_report)
+		die("die with the --die-write-report option");
 	if (returns.nr)
 		for_each_string_list_item(item, &returns)
 			packet_write_fmt(1, "%s\n", item->string);
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index 854c3e884a..b9be12be77 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -16,7 +16,8 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 
 	# Check status report for git-push
 	sed -n \
-		-e "/^To / { p; n; p; }" \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
 		<actual >actual-report &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
@@ -41,32 +42,98 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 	test_cmp expect actual
 '
 
-test_expect_success "setup proc-receive hook (hook --die-version, $PROTOCOL)" '
+test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
-	test-tool proc-receive -v --die-version
+	test-tool proc-receive -v --die-read-version
 	EOF
 '
 
 # Refs of upstream : main(A)
 # Refs of workbench: main(A)  tags/v123
 # git push         :                       refs/for/main/topic(A)
-test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)" '
+test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-version
+	EOF
+'
 
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-commands
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
 	cat >expect <<-EOF &&
-	remote: # pre-receive hook
-	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
-	remote: # proc-receive hook
-	remote: fatal: bad protocol version: 1
-	remote: error: proc-receive version "0" is not supported
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-commands option" out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -76,23 +143,65 @@ test_expect_success "proc-receive: bad protocol (hook --die-version, $PROTOCOL)"
 	test_cmp expect actual
 '
 
-test_expect_success "setup proc-receive hook (hook --die-readline, $PROTOCOL)" '
+test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
-	test-tool proc-receive -v --die-readline
+	test-tool proc-receive -v --die-read-push-options
 	EOF
 '
 
 # Refs of upstream : main(A)
 # Refs of workbench: main(A)  tags/v123
 # git push         :                       refs/for/main/topic(A)
-test_expect_success "proc-receive: bad protocol (hook --die-readline, $PROTOCOL)" '
+test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $PROTOCOL)" '
+	git -C "$upstream" config receive.advertisePushOptions true &&
 	test_must_fail git -C workbench push origin \
+		-o reviewers=user1,user2 \
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-push-options option" out &&
+
+	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-report
+	EOF
+'
 
-	grep "remote: fatal: protocol error: expected \"old new ref\", got \"<ZERO-OID> <COMMIT-A> refs/for/main/topic\"" actual &&
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL)" '
+	test_must_fail git -C workbench push origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; }" \
+		-e "/^ ! / { p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-report option" out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -130,6 +239,7 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -173,6 +283,7 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -208,6 +319,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index 88c56311da..fdb4569109 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -42,6 +42,175 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porc
 	test_cmp expect actual
 '
 
+test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-version
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-version
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-version option" out &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-commands
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-commands option" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-read-push-options
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $PROTOCOL/porcelain)" '
+	git -C "$upstream" config receive.advertisePushOptions true &&
+	test_must_fail git -C workbench push --porcelain origin \
+		-o reviewers=user1,user2 \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-read-push-options option" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v --die-write-report
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       refs/for/main/topic(A)
+test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL/porcelain)" '
+	test_must_fail git -C workbench push --porcelain origin \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	filter_out_user_friendly_and_stable_output \
+		-e "/^To / { p; n; p; n; p; }" \
+		<out >actual &&
+	cat >expect <<-EOF &&
+	To <URL/of/upstream.git>
+	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
+	Done
+	EOF
+	test_cmp expect actual &&
+	grep "remote: fatal: die with the --die-write-report option" out &&
+
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	EOF
+	test_cmp expect actual
+'
+
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
 	write_script "$upstream/hooks/proc-receive" <<-EOF
 	printf >&2 "# proc-receive hook\n"
@@ -71,6 +240,7 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 	Done
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -84,7 +254,6 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 # Refs of workbench: main(A)  tags/v123
 test_expect_success "cleanup ($PROTOCOL/porcelain)" '
 	git -C "$upstream" update-ref -d refs/heads/next
-
 '
 
 test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
@@ -115,6 +284,7 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
@@ -151,6 +321,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porce
 	Done
 	EOF
 	test_cmp expect actual &&
+
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v4 3/3] receive-pack: use default version 0 for proc-receive
  2020-11-10 11:49                                                                 ` Jiang Xin
                                                                                     ` (5 preceding siblings ...)
  2020-11-11 11:32                                                                   ` [PATCH v4 2/3] receive-pack: gently write messages to proc-receive Jiang Xin
@ 2020-11-11 11:32                                                                   ` Jiang Xin
  6 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2020-11-11 11:32 UTC (permalink / raw)
  To: Junio C Hamano, Git List, Johannes Schindelin, Jeff King; +Cc: Jiang Xin

In the verison negotiation phase between "receive-pack" and
"proc-receive", "proc-receive" can send an empty flush-pkt to end the
negotiation and use default version 0. Capabilities (such as
"push-options") are not supported in version 0.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 builtin/receive-pack.c                       |  7 ++-
 t/helper/test-proc-receive.c                 | 16 +++--
 t/t5411/test-0026-push-options.sh            | 60 +++++++++++++++++++
 t/t5411/test-0027-push-options--porcelain.sh | 62 ++++++++++++++++++++
 4 files changed, 138 insertions(+), 7 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 2bd736525f..f1f0f7bef6 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1192,7 +1192,12 @@ static int run_proc_receive_hook(struct command *commands,
 		goto cleanup;
 	}
 
-	if (version != 1) {
+	switch (version) {
+	case 0:
+		/* fallthrough */
+	case 1:
+		break;
+	default:
 		strbuf_addf(&errmsg, "proc-receive version '%d' is not supported",
 			    version);
 		code = -1;
diff --git a/t/helper/test-proc-receive.c b/t/helper/test-proc-receive.c
index 6652cedcee..cc08506cf0 100644
--- a/t/helper/test-proc-receive.c
+++ b/t/helper/test-proc-receive.c
@@ -45,8 +45,14 @@ static void proc_receive_verison(struct packet_reader *reader) {
 		if (packet_reader_read(reader) != PACKET_READ_NORMAL)
 			break;
 
+		/* Ignore version negotiation for version 0 */
+		if (version == 0)
+			continue;
+
 		if (reader->pktlen > 8 && starts_with(reader->line, "version=")) {
 			server_version = atoi(reader->line+8);
+			if (server_version != 1)
+				die("bad protocol version: %d", server_version);
 			linelen = strlen(reader->line);
 			if (linelen < reader->pktlen) {
 				const char *feature_list = reader->line + linelen + 1;
@@ -58,15 +64,13 @@ static void proc_receive_verison(struct packet_reader *reader) {
 		}
 	}
 
-	if (server_version != 1)
-		die("bad protocol version: %d", server_version);
-
 	if (die_write_version)
 		die("die with the --die-write-version option");
 
-	packet_write_fmt(1, "version=%d%c%s\n",
-			 version, '\0',
-			 use_push_options && !no_push_options ? "push-options": "");
+	if (version != 0)
+		packet_write_fmt(1, "version=%d%c%s\n",
+				 version, '\0',
+				 use_push_options && !no_push_options ? "push-options": "");
 	packet_flush(1);
 }
 
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index d414be87d0..e88edb16a4 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -32,6 +32,66 @@ test_expect_success "enable push options ($PROTOCOL)" '
 	git -C "$upstream" config receive.advertisePushOptions true
 '
 
+test_expect_success "setup version=0 for proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		--version 0 \
+		-r "ok refs/for/main/topic"
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/main/topic
+test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL)" '
+	git -C workbench push \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	remote: proc-receive> ok refs/for/main/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 * [new reference] HEAD -> refs/for/main/topic
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "restore proc-receive hook ($PROTOCOL)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/main/topic"
+	EOF
+'
+
+# Refs of upstream : main(A)             next(A)
+# Refs of workbench: main(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
 # Refs of upstream : main(A)
 # Refs of workbench: main(A)  tags/v123
 # git push -o ...  :                       next(A)  refs/for/main/topic
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index d5d0dcb172..3a6561b5ea 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -33,6 +33,68 @@ test_expect_success "enable push options ($PROTOCOL/porcelain)" '
 	git -C "$upstream" config receive.advertisePushOptions true
 '
 
+test_expect_success "setup version=0 for proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		--version 0 \
+		-r "ok refs/for/main/topic"
+	EOF
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push -o ...  :                       next(A)  refs/for/main/topic
+test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL/porcelain)" '
+	git -C workbench push \
+		--porcelain \
+		--atomic \
+		-o issue=123 \
+		-o reviewer=user1 \
+		origin \
+		HEAD:refs/heads/next \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	remote: # proc-receive hook
+	remote: proc-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	remote: proc-receive> ok refs/for/main/topic
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	To <URL/of/upstream.git>
+	*    HEAD:refs/heads/next    [new branch]
+	*    HEAD:refs/for/main/topic    [new reference]
+	Done
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success "restore proc-receive hook ($PROTOCOL/porcelain)" '
+	write_script "$upstream/hooks/proc-receive" <<-EOF
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/main/topic"
+	EOF
+'
+
+# Refs of upstream : main(A)             next(A)
+# Refs of workbench: main(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL/porcelain)" '
+	git -C "$upstream" update-ref -d refs/heads/next
+'
+
 # Refs of upstream : main(A)
 # Refs of workbench: main(A)  tags/v123
 # git push -o ...  :                       next(A)  refs/for/main/topic
-- 
2.29.0.dirty


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2020-08-27 15:45                                             ` [PATCH v19 03/10] receive-pack: add new " Jiang Xin
  2020-11-04 22:15                                               ` Johannes Schindelin
@ 2021-01-17 22:21                                               ` SZEDER Gábor
  2021-01-18  8:24                                                 ` Jiang Xin
                                                                   ` (2 more replies)
  1 sibling, 3 replies; 266+ messages in thread
From: SZEDER Gábor @ 2021-01-17 22:21 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Git List, Jiang Xin


This patch added a whole lot of test cases, and two of them '86 -
proc-receive: not support push options (builtin protocol)' and '95 -
proc-receive: not support push options (builtin protocol/porcelain)'
are prone to rare failures.

On Thu, Aug 27, 2020 at 11:45:44AM -0400, Jiang Xin wrote:
> diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
> new file mode 100644
> index 0000000000..d0c4da8b23
> --- /dev/null
> +++ b/t/t5411/test-0026-push-options.sh

> +# Refs of upstream : master(A)
> +# Refs of workbench: master(A)  tags/v123
> +# git push -o ...  :                       refs/for/master/topic
> +test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
> +	test_must_fail git -C workbench push \
> +		-o issue=123 \
> +		-o reviewer=user1 \
> +		origin \
> +		HEAD:refs/for/master/topic \
> +		>out 2>&1 &&

Three relevant things are happening here:

  - 'git push' is executed with its standard output and error
    redirected to the file 'out'.

  - 'git push' executes 'git receive-pack' internally, which inherits
    the open file descriptors, so its output and error goes into that
    same 'out' file.

  - 'git push' is expected to fail when it finds out that the other
    side doesn't support push options, but it does so with a simple
    die() right away, without waiting for its child 'git receive-pack'
    process to finish.

> +	make_user_friendly_and_stable_output <out >actual &&
> +	test_i18ngrep "fatal: the receiving end does not support push options" \
> +		actual &&
> +	git -C "$upstream" show-ref >out &&

Here the shell opens and truncates the file 'out' to write 'git
show-ref's output, i.e. it is still the same 'out' file that was used
earlier.

Consequently, it is possible that 'git receive-pack' is still running,
its open file descriptors to 'out' are still valid, and its "fatal:
the remote end hung up unexpectedly" error message about the suddenly
disappeared 'git push' can partially overwrite the output from 'git
show-ref'.

> +	make_user_friendly_and_stable_output <out >actual &&
> +	cat >expect <<-EOF &&
> +	<COMMIT-A> refs/heads/master
> +	EOF
> +	test_cmp expect actual
> +'

The supposedly stable output can then unexpectedly include that error
message, and the test fails with something like this:

  + test_cmp expect actual
  --- expect      2021-01-17 21:55:23.430750004 +0000
  +++ actual      2021-01-17 21:55:23.430750004 +0000
  @@ -1 +1 @@
  -<COMMIT-A> refs/heads/main
  +<COMMIT-A> refs/heads/maifatal: the remote end hung up unexpectedly
  error: last command exited with $?=1
  not ok 86 - proc-receive: not support push options (builtin protocol)

> diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
> new file mode 100644
> index 0000000000..c89a1e7c57
> --- /dev/null
> +++ b/t/t5411/test-0027-push-options--porcelain.sh

> +test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain)" '
> +	test_must_fail git -C workbench push \
> +		--porcelain \
> +		-o issue=123 \
> +		-o reviewer=user1 \
> +		origin \
> +		HEAD:refs/for/master/topic \
> +		>out 2>&1 &&
> +	make_user_friendly_and_stable_output <out >actual &&
> +	test_i18ngrep "fatal: the receiving end does not support push options" \
> +		actual &&
> +	git -C "$upstream" show-ref >out &&
> +	make_user_friendly_and_stable_output <out >actual &&
> +	cat >expect <<-EOF &&
> +	<COMMIT-A> refs/heads/master
> +	EOF
> +	test_cmp expect actual
> +'

This test is almost identical to the one above, and the same sequence
of events leads to a similar failure.


I saw these tests fail in Travis CI's s390x environment a couple of
times, and, alas, that is the only environment where I was able to
reproduce the failure with '--stress' with an unmodified Git.

The diff below adds a couple of strategically-placed delays to
reliably demonstrate these failures:

  ---  >8  ---

diff --git a/pkt-line.c b/pkt-line.c
index d633005ef7..3b26631948 100644
--- a/pkt-line.c
+++ b/pkt-line.c
@@ -307,6 +307,7 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,
 		if (options & PACKET_READ_GENTLE_ON_EOF)
 			return -1;
 
+		sleep(1);
 		die(_("the remote end hung up unexpectedly"));
 	}
 
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index e88edb16a4..a1e896f404 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -21,6 +21,7 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
+	sleep 2 &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	<COMMIT-A> refs/heads/main
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index 3a6561b5ea..f97cef440f 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -22,6 +22,7 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
+	sleep 2 &&
 	make_user_friendly_and_stable_output <out >actual &&
 	cat >expect <<-EOF &&
 	<COMMIT-A> refs/heads/main

  ---  8<  ---

I think these are the only two tests that can cause this racy
behavior: by instrumenting finish_command() I found that in all other
tests where 'git push' is expected to fail it errors out gracefully
and waits for its 'git receive-pack' child process.



^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2021-01-17 22:21                                               ` SZEDER Gábor
@ 2021-01-18  8:24                                                 ` Jiang Xin
  2021-01-20 12:28                                                   ` SZEDER Gábor
  2021-01-18 13:30                                                 ` [PATCH 1/2] t5411: remove file after use to prevent overwriting Jiang Xin
  2021-01-18 13:30                                                 ` [PATCH " Jiang Xin
  2 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2021-01-18  8:24 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: Junio C Hamano, Git List, Jiang Xin

SZEDER Gábor <szeder.dev@gmail.com> 于2021年1月18日周一 上午6:21写道:
>
>
> This patch added a whole lot of test cases, and two of them '86 -
> proc-receive: not support push options (builtin protocol)' and '95 -
> proc-receive: not support push options (builtin protocol/porcelain)'
> are prone to rare failures.
>
> On Thu, Aug 27, 2020 at 11:45:44AM -0400, Jiang Xin wrote:
> > diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
> > new file mode 100644
> > index 0000000000..d0c4da8b23
> > --- /dev/null
> > +++ b/t/t5411/test-0026-push-options.sh
>
> > +# Refs of upstream : master(A)
> > +# Refs of workbench: master(A)  tags/v123
> > +# git push -o ...  :                       refs/for/master/topic
> > +test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
> > +     test_must_fail git -C workbench push \
> > +             -o issue=123 \
> > +             -o reviewer=user1 \
> > +             origin \
> > +             HEAD:refs/for/master/topic \
> > +             >out 2>&1 &&
>
> Three relevant things are happening here:
>
>   - 'git push' is executed with its standard output and error
>     redirected to the file 'out'.
>
>   - 'git push' executes 'git receive-pack' internally, which inherits
>     the open file descriptors, so its output and error goes into that
>     same 'out' file.
>
>   - 'git push' is expected to fail when it finds out that the other
>     side doesn't support push options, but it does so with a simple
>     die() right away, without waiting for its child 'git receive-pack'
>     process to finish.
>
> > +     make_user_friendly_and_stable_output <out >actual &&
> > +     test_i18ngrep "fatal: the receiving end does not support push options" \
> > +             actual &&
> > +     git -C "$upstream" show-ref >out &&
>
> Here the shell opens and truncates the file 'out' to write 'git
> show-ref's output, i.e. it is still the same 'out' file that was used
> earlier.
>
> Consequently, it is possible that 'git receive-pack' is still running,
> its open file descriptors to 'out' are still valid, and its "fatal:
> the remote end hung up unexpectedly" error message about the suddenly
> disappeared 'git push' can partially overwrite the output from 'git
> show-ref'.

Thank you for providing the accurate analysis of the root cause of this problem.

> I saw these tests fail in Travis CI's s390x environment a couple of
> times, and, alas, that is the only environment where I was able to
> reproduce the failure with '--stress' with an unmodified Git.
>
> The diff below adds a couple of strategically-placed delays to
> reliably demonstrate these failures:

To solve this problem, instead of adding delays, we can simply remove
the "out" file before creating a new one, thus the two "out" files
have two different file descriptors, and the process in background
won't break the new "out" file.

>
>   ---  >8  ---
>
> diff --git a/pkt-line.c b/pkt-line.c
> index d633005ef7..3b26631948 100644
> --- a/pkt-line.c
> +++ b/pkt-line.c
> @@ -307,6 +307,7 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,
>                 if (options & PACKET_READ_GENTLE_ON_EOF)
>                         return -1;
>
> +               sleep(1);
>                 die(_("the remote end hung up unexpectedly"));
>         }
>
> diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
> index e88edb16a4..a1e896f404 100644
> --- a/t/t5411/test-0026-push-options.sh
> +++ b/t/t5411/test-0026-push-options.sh
> @@ -21,6 +21,7 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
>         test_i18ngrep "fatal: the receiving end does not support push options" \
>                 actual &&
>         git -C "$upstream" show-ref >out &&
> +       sleep 2 &&
>         make_user_friendly_and_stable_output <out >actual &&
>         cat >expect <<-EOF &&
>         <COMMIT-A> refs/heads/main
> diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
> index 3a6561b5ea..f97cef440f 100644
> --- a/t/t5411/test-0027-push-options--porcelain.sh
> +++ b/t/t5411/test-0027-push-options--porcelain.sh
> @@ -22,6 +22,7 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
>         test_i18ngrep "fatal: the receiving end does not support push options" \
>                 actual &&
>         git -C "$upstream" show-ref >out &&
> +       sleep 2 &&
>         make_user_friendly_and_stable_output <out >actual &&
>         cat >expect <<-EOF &&
>         <COMMIT-A> refs/heads/main
>
>   ---  8<  ---
>
> I think these are the only two tests that can cause this racy
> behavior: by instrumenting finish_command() I found that in all other
> tests where 'git push' is expected to fail it errors out gracefully
> and waits for its 'git receive-pack' child process.

Atomic push may have the same problem.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH 1/2] t5411: remove file after use to prevent overwriting
  2021-01-17 22:21                                               ` SZEDER Gábor
  2021-01-18  8:24                                                 ` Jiang Xin
@ 2021-01-18 13:30                                                 ` Jiang Xin
  2021-01-18 18:21                                                   ` Johannes Sixt
  2021-01-18 13:30                                                 ` [PATCH " Jiang Xin
  2 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2021-01-18 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

SZEDER reported that t5411 failed in Travis CI's s390x environment a
couple of times, and could be reproduced with '--stress' test on this
specific environment.  The test failure messages might look like this:

    + test_cmp expect actual
    --- expect      2021-01-17 21:55:23.430750004 +0000
    +++ actual      2021-01-17 21:55:23.430750004 +0000
    @@ -1 +1 @@
    -<COMMIT-A> refs/heads/main
    +<COMMIT-A> refs/heads/maifatal: the remote end hung up unexpectedly
    error: last command exited with $?=1
    not ok 86 - proc-receive: not support push options (builtin protocol)

The file 'actual' is filtered from the file 'out' which contains result
of 'git show-ref' command.  Due to the error messages from other process
is written into the file 'out' accidentally, t5411 failed.  SZEDER finds
the root cause of this issue:

 - 'git push' is executed with its standard output and error redirected
   to the file 'out'.

 - 'git push' executes 'git receive-pack' internally, which inherits
   the open file descriptors, so its output and error goes into that
   same 'out' file.

 - 'git push' ends without waiting for the close of 'git-receive-pack'
   for some cases, and the file 'out' is reused for test of
   'git show-ref' afterwards.

 - A mixture of the output of 'git show-ref' abd 'git receive-pack'
   leads to this issue.

To resolve this issue, we can remove the file 'out' after use.  The
long-running 'git receive-pack' will not redirect its output to the new
created 'out' file which has a different file descriptor.

Reported-by: SZEDER Gábor <szeder.dev@gmail.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/test-0000-standard-git-push.sh         |  4 ++++
 .../test-0001-standard-git-push--porcelain.sh  |  4 ++++
 t/t5411/test-0002-pre-receive-declined.sh      |  2 ++
 ...est-0003-pre-receive-declined--porcelain.sh |  2 ++
 t/t5411/test-0011-no-hook-error.sh             |  4 ++++
 t/t5411/test-0012-no-hook-error--porcelain.sh  |  4 ++++
 t/t5411/test-0013-bad-protocol.sh              | 18 ++++++++++++++++++
 t/t5411/test-0014-bad-protocol--porcelain.sh   | 18 ++++++++++++++++++
 t/t5411/test-0020-report-ng.sh                 |  4 ++++
 t/t5411/test-0021-report-ng--porcelain.sh      |  4 ++++
 t/t5411/test-0022-report-unexpect-ref.sh       |  2 ++
 ...test-0023-report-unexpect-ref--porcelain.sh |  2 ++
 t/t5411/test-0024-report-unknown-ref.sh        |  2 ++
 .../test-0025-report-unknown-ref--porcelain.sh |  2 ++
 t/t5411/test-0026-push-options.sh              |  2 ++
 t/t5411/test-0027-push-options--porcelain.sh   |  2 ++
 t/t5411/test-0032-report-with-options.sh       |  2 ++
 ...test-0033-report-with-options--porcelain.sh |  2 ++
 t/t5411/test-0038-report-mixed-refs.sh         |  2 ++
 .../test-0039-report-mixed-refs--porcelain.sh  |  2 ++
 20 files changed, 84 insertions(+)

diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index 47b058af7e..694d8e8dc2 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -40,6 +40,8 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
 		<out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [rejected] main -> main (non-fast-forward)
@@ -67,6 +69,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
 		$B:refs/heads/next \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index bbead12bbb..6cf0bc7034 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -42,6 +42,8 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 		-e "/^To / { p; }" \
 		-e "/^! / { p; }" \
 		<out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    refs/heads/main:refs/heads/main    [rejected] (non-fast-forward)
@@ -69,6 +71,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
 		$B:refs/heads/next \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
index e7d113a158..61c3ee1b7f 100644
--- a/t/t5411/test-0002-pre-receive-declined.sh
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -14,6 +14,8 @@ test_expect_success "git-push is declined ($PROTOCOL)" '
 		HEAD:refs/heads/next \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] <COMMIT-B> -> main (pre-receive hook declined)
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
index cc0cca6a47..0a9cc2d44a 100644
--- a/t/t5411/test-0003-pre-receive-declined--porcelain.sh
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -14,6 +14,8 @@ test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
 		HEAD:refs/heads/next \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    <COMMIT-B>:refs/heads/main    [remote rejected] (pre-receive hook declined)
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
index c50830982f..fd2bd20b76 100644
--- a/t/t5411/test-0011-no-hook-error.sh
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -7,6 +7,8 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -43,6 +45,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 		HEAD:next \
 		HEAD:refs/for/main/topic >out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
index 14ea433481..025a9c7186 100644
--- a/t/t5411/test-0012-no-hook-error--porcelain.sh
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -7,6 +7,8 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -44,6 +46,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 		HEAD:next \
 		HEAD:refs/for/main/topic >out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index b9be12be77..d20a4e6681 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -13,6 +13,8 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	# Check status report for git-push
 	sed -n \
@@ -67,6 +69,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-version option" out &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -101,6 +105,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-version option" out &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -134,6 +140,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	EOF
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-commands option" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -169,6 +177,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	EOF
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-push-options option" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -202,6 +212,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	EOF
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-report option" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -226,6 +238,8 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 		HEAD:refs/heads/next \
 		HEAD:refs/for/main/topic >out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -272,6 +286,8 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 		HEAD:refs/for/main/topic\
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -308,6 +324,8 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 			HEAD:refs/for/main/topic \
 			>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index fdb4569109..a286030f3e 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -13,6 +13,8 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porc
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	# Check status report for git-push
 	sed -n \
@@ -67,6 +69,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-version option" out &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -101,6 +105,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-version option" out &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -134,6 +140,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	EOF
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-commands option" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -169,6 +177,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	EOF
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-push-options option" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -202,6 +212,8 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	EOF
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-report option" out &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -226,6 +238,8 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 		HEAD:refs/heads/next \
 		HEAD:refs/for/main/topic >out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -272,6 +286,8 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 		HEAD:refs/for/main/topic\
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -309,6 +325,8 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porce
 			HEAD:refs/for/main/topic \
 			>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
index 5a9e0daf2d..9f71da9fb6 100644
--- a/t/t5411/test-0020-report-ng.sh
+++ b/t/t5411/test-0020-report-ng.sh
@@ -14,6 +14,8 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -48,6 +50,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)"
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
index 93475a83cf..96065e3537 100644
--- a/t/t5411/test-0021-report-ng--porcelain.sh
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -14,6 +14,8 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/por
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -49,6 +51,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/p
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
index f8be8a0ba1..624dda7acc 100644
--- a/t/t5411/test-0022-report-unexpect-ref.sh
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -15,6 +15,8 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
index 778150fa03..dcb804afc8 100644
--- a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -15,6 +15,8 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)"
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
index d4e74e4681..af65355862 100644
--- a/t/t5411/test-0024-report-unknown-ref.sh
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -14,6 +14,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
 		HEAD:refs/for/a/b/c/my/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
index 039e8b6163..c336ec0e98 100644
--- a/t/t5411/test-0025-report-unknown-ref--porcelain.sh
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -14,6 +14,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain
 		HEAD:refs/for/a/b/c/my/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index e88edb16a4..46b1064fe6 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -18,6 +18,8 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index 3a6561b5ea..79a49a638e 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -19,6 +19,8 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a0faf5c7ff..4d058cc6ff 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -15,6 +15,8 @@ test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 32ae26bcfb..8ab908a6b0 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -15,6 +15,8 @@ test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/
 		HEAD:refs/for/main/topic \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 0d071ebaa6..264a5d5c6d 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -26,6 +26,8 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 		HEAD:refs/for/next/topic3 \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index d8409912fd..1e9c06d2b7 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -26,6 +26,8 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 		HEAD:refs/for/next/topic3 \
 		>out 2>&1 &&
 	make_user_friendly_and_stable_output <out >actual &&
+	# Prevent accidential changes by the internal "receive-pack" process.
+	rm out &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH 2/2] t5411: refactor check of refs using test_cmp_refs
  2021-01-17 22:21                                               ` SZEDER Gábor
  2021-01-18  8:24                                                 ` Jiang Xin
  2021-01-18 13:30                                                 ` [PATCH 1/2] t5411: remove file after use to prevent overwriting Jiang Xin
@ 2021-01-18 13:30                                                 ` Jiang Xin
  2 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-01-18 13:30 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new helper 'test_cmp_refs' to check references in a repository.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/common-functions.sh                   | 15 +++++++
 t/t5411/once-0010-report-status-v1.sh         |  5 +--
 t/t5411/test-0000-standard-git-push.sh        | 24 ++++------
 .../test-0001-standard-git-push--porcelain.sh | 24 ++++------
 t/t5411/test-0002-pre-receive-declined.sh     |  6 +--
 ...st-0003-pre-receive-declined--porcelain.sh |  6 +--
 t/t5411/test-0011-no-hook-error.sh            | 12 ++---
 t/t5411/test-0012-no-hook-error--porcelain.sh | 12 ++---
 t/t5411/test-0013-bad-protocol.sh             | 45 ++++---------------
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 45 ++++---------------
 t/t5411/test-0020-report-ng.sh                | 12 ++---
 t/t5411/test-0021-report-ng--porcelain.sh     | 12 ++---
 t/t5411/test-0022-report-unexpect-ref.sh      |  6 +--
 ...est-0023-report-unexpect-ref--porcelain.sh |  6 +--
 t/t5411/test-0024-report-unknown-ref.sh       |  6 +--
 ...test-0025-report-unknown-ref--porcelain.sh |  6 +--
 t/t5411/test-0026-push-options.sh             | 18 +++-----
 t/t5411/test-0027-push-options--porcelain.sh  | 18 +++-----
 t/t5411/test-0030-report-ok.sh                |  6 +--
 t/t5411/test-0031-report-ok--porcelain.sh     |  6 +--
 t/t5411/test-0032-report-with-options.sh      |  5 +--
 ...est-0033-report-with-options--porcelain.sh |  5 +--
 t/t5411/test-0034-report-ft.sh                |  6 +--
 t/t5411/test-0035-report-ft--porcelain.sh     |  6 +--
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 18 +++-----
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 18 +++-----
 t/t5411/test-0038-report-mixed-refs.sh        |  6 +--
 .../test-0039-report-mixed-refs--porcelain.sh |  6 +--
 t/t5411/test-0040-process-all-refs.sh         |  6 +--
 .../test-0041-process-all-refs--porcelain.sh  |  6 +--
 ...t-0050-proc-receive-refs-with-modifiers.sh | 18 +++-----
 31 files changed, 126 insertions(+), 264 deletions(-)

diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 344d13f61a..4835dd5182 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -59,3 +59,18 @@ filter_out_user_friendly_and_stable_output () {
 	make_user_friendly_and_stable_output |
 		sed -n ${1+"$@"}
 }
+
+test_cmp_refs () {
+	indir=
+	if test "$1" = "-C"
+	then
+		shift
+		indir="$1"
+		shift
+	fi
+	indir=${indir:+"$indir"/}
+	cat >show-ref.expect &&
+	git ${indir:+ -C "$indir"} show-ref >show-ref.pristine &&
+	make_user_friendly_and_stable_output <show-ref.pristine >show-ref.filtered &&
+	test_cmp show-ref.expect show-ref.filtered
+}
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index cb431a9c91..1233a46eac 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -83,12 +83,9 @@ test_expect_success "proc-receive: report status v1" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/for/main/topic1
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index 694d8e8dc2..7af6026749 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -19,13 +19,11 @@ test_expect_success "git-push ($PROTOCOL)" '
 	 * [new branch] HEAD -> next
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -48,13 +46,11 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 	 ! [rejected] <COMMIT-B> -> next (atomic push failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -81,13 +77,11 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
 	 ! [rejected] main -> main (non-fast-forward)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-B> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(B)
@@ -123,15 +117,13 @@ test_expect_success "git-push -f ($PROTOCOL)" '
 	 * [new branch] HEAD -> a/b/c
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/a/b/c
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/review/main/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)  tags/v123  refs/review/main/topic(A)  a/b/c(A)
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index 6cf0bc7034..9e5c40f39c 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -20,13 +20,11 @@ test_expect_success "git-push ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -50,13 +48,11 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 	!    <COMMIT-B>:refs/heads/next    [rejected] (atomic push failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -84,13 +80,11 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-B> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(B)
@@ -127,15 +121,13 @@ test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/a/b/c
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/review/main/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)  tags/v123  refs/review/main/topic(A)  a/b/c(A)
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
index 61c3ee1b7f..7c70d45057 100644
--- a/t/t5411/test-0002-pre-receive-declined.sh
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -22,12 +22,10 @@ test_expect_success "git-push is declined ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "cleanup ($PROTOCOL)" '
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
index 0a9cc2d44a..ea38248fc5 100644
--- a/t/t5411/test-0003-pre-receive-declined--porcelain.sh
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -23,12 +23,10 @@ test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "cleanup ($PROTOCOL/porcelain)" '
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
index fd2bd20b76..778b833f9d 100644
--- a/t/t5411/test-0011-no-hook-error.sh
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -21,13 +21,11 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -59,10 +57,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
index 025a9c7186..371831d170 100644
--- a/t/t5411/test-0012-no-hook-error--porcelain.sh
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -22,13 +22,11 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -61,10 +59,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index d20a4e6681..43d37f4109 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -36,12 +36,9 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual-error &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL)" '
@@ -72,12 +69,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL)" '
@@ -108,12 +102,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL)" '
@@ -143,12 +134,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL)" '
@@ -180,12 +168,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL)" '
@@ -215,12 +200,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
@@ -254,13 +236,10 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -300,12 +279,9 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
@@ -338,10 +314,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index a286030f3e..c6299b4849 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -36,12 +36,9 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porc
 	EOF
 	test_cmp expect actual-error &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL/porcelain)" '
@@ -72,12 +69,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL/porcelain)" '
@@ -108,12 +102,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL/porcelain)" '
@@ -143,12 +134,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL/porcelain)" '
@@ -180,12 +168,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL/porcelain)" '
@@ -215,12 +200,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	# Prevent accidential changes by the internal "receive-pack" process.
 	rm out &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
@@ -255,13 +237,10 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -301,12 +280,9 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelain)" '
@@ -340,10 +316,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porce
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
index 9f71da9fb6..37300d374b 100644
--- a/t/t5411/test-0020-report-ng.sh
+++ b/t/t5411/test-0020-report-ng.sh
@@ -26,12 +26,10 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
@@ -62,10 +60,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)"
 	 ! [remote rejected] HEAD -> refs/for/main/topic (error msg)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
index 96065e3537..c40eb1060b 100644
--- a/t/t5411/test-0021-report-ng--porcelain.sh
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -27,12 +27,10 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/por
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)" '
@@ -64,10 +62,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/p
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
index 624dda7acc..6c1716b25a 100644
--- a/t/t5411/test-0022-report-unexpect-ref.sh
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -32,12 +32,10 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
index dcb804afc8..46f44cdad5 100644
--- a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -33,12 +33,10 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)"
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
index af65355862..a543c5acf7 100644
--- a/t/t5411/test-0024-report-unknown-ref.sh
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -27,10 +27,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
index c336ec0e98..972df50ca7 100644
--- a/t/t5411/test-0025-report-unknown-ref--porcelain.sh
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -28,10 +28,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index 46b1064fe6..049a474a0c 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -22,12 +22,10 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
 	rm out &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "enable push options ($PROTOCOL)" '
@@ -71,13 +69,11 @@ test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL)
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "restore proc-receive hook ($PROTOCOL)" '
@@ -125,13 +121,11 @@ test_expect_success "proc-receive: push with options ($PROTOCOL)" '
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index 79a49a638e..e0455b8b81 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -23,12 +23,10 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
 	rm out &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "enable push options ($PROTOCOL/porcelain)" '
@@ -74,13 +72,11 @@ test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL/
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "restore proc-receive hook ($PROTOCOL/porcelain)" '
@@ -130,13 +126,11 @@ test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
index 5d6feef118..8acb4f204f 100644
--- a/t/t5411/test-0030-report-ok.sh
+++ b/t/t5411/test-0030-report-ok.sh
@@ -26,10 +26,8 @@ test_expect_success "proc-receive: ok ($PROTOCOL)" '
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
index 91666d32df..a967718046 100644
--- a/t/t5411/test-0031-report-ok--porcelain.sh
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -27,10 +27,8 @@ test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index 4d058cc6ff..9711d69f1d 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -249,10 +249,7 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 8ab908a6b0..7e699e8088 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -258,10 +258,7 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
index c355c290d2..6e0d08b327 100644
--- a/t/t5411/test-0034-report-ft.sh
+++ b/t/t5411/test-0034-report-ft.sh
@@ -28,13 +28,11 @@ test_expect_success "proc-receive: fall throught, let receive-pack to execute ($
 	 * [new reference] <COMMIT-B> -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/for/main/topic
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             refs/for/main/topic(A)
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
index 8ce4e58f2a..81bae9f2ec 100644
--- a/t/t5411/test-0035-report-ft--porcelain.sh
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -29,13 +29,11 @@ test_expect_success "proc-receive: fall throught, let receive-pack to execute ($
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/for/main/topic
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             refs/for/main/topic(A)
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index fad8eea8a0..be9b18b2b6 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -65,12 +65,10 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
@@ -142,12 +140,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
@@ -205,12 +201,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index dc254d57eb..95fb89c031 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -51,12 +51,10 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL/porcelain)" '
@@ -114,12 +112,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL/porcelain)" '
@@ -163,10 +159,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 264a5d5c6d..88fa60e53c 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -67,15 +67,13 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)             foo(A)  bar(A))  baz(A)
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 1e9c06d2b7..6c36d9250f 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -68,15 +68,13 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)             foo(A)  bar(A))  baz(A)
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
index 2565302a17..fdcdcc7c2e 100644
--- a/t/t5411/test-0040-process-all-refs.sh
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -92,14 +92,12 @@ test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
 	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             bar(A)  baz(B)
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
index e21420b60d..73b35fe0aa 100644
--- a/t/t5411/test-0041-process-all-refs--porcelain.sh
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -93,14 +93,12 @@ test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             bar(A)  baz(B)
diff --git a/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
index 2e29518ec5..7214647ada 100644
--- a/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
+++ b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
@@ -50,12 +50,10 @@ test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
 	 * [new reference] v123 -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)
@@ -63,14 +61,12 @@ test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
 test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
 	git -C "$upstream" update-ref refs/heads/topic $A &&
 	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook ($PROTOCOL)" '
@@ -125,11 +121,9 @@ test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOC
 	 * [new reference] <COMMIT-A> -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-B> refs/heads/topic
 	EOF
-	test_cmp expect actual
 '
-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH 1/2] t5411: remove file after use to prevent overwriting
  2021-01-18 13:30                                                 ` [PATCH 1/2] t5411: remove file after use to prevent overwriting Jiang Xin
@ 2021-01-18 18:21                                                   ` Johannes Sixt
  2021-01-19  0:48                                                     ` Jiang Xin
                                                                       ` (3 more replies)
  0 siblings, 4 replies; 266+ messages in thread
From: Johannes Sixt @ 2021-01-18 18:21 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Jiang Xin, Junio C Hamano, Git List, SZEDER Gábor

Am 18.01.21 um 14:30 schrieb Jiang Xin:
> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> 
> SZEDER reported that t5411 failed in Travis CI's s390x environment a
> couple of times, and could be reproduced with '--stress' test on this
> specific environment.  The test failure messages might look like this:
> 
>     + test_cmp expect actual
>     --- expect      2021-01-17 21:55:23.430750004 +0000
>     +++ actual      2021-01-17 21:55:23.430750004 +0000
>     @@ -1 +1 @@
>     -<COMMIT-A> refs/heads/main
>     +<COMMIT-A> refs/heads/maifatal: the remote end hung up unexpectedly
>     error: last command exited with $?=1
>     not ok 86 - proc-receive: not support push options (builtin protocol)
> 
> The file 'actual' is filtered from the file 'out' which contains result
> of 'git show-ref' command.  Due to the error messages from other process
> is written into the file 'out' accidentally, t5411 failed.  SZEDER finds
> the root cause of this issue:
> 
>  - 'git push' is executed with its standard output and error redirected
>    to the file 'out'.
> 
>  - 'git push' executes 'git receive-pack' internally, which inherits
>    the open file descriptors, so its output and error goes into that
>    same 'out' file.
> 
>  - 'git push' ends without waiting for the close of 'git-receive-pack'
>    for some cases, and the file 'out' is reused for test of
>    'git show-ref' afterwards.
> 
>  - A mixture of the output of 'git show-ref' abd 'git receive-pack'
>    leads to this issue.
> 
> To resolve this issue, we can remove the file 'out' after use.  The
> long-running 'git receive-pack' will not redirect its output to the new
> created 'out' file which has a different file descriptor.

On Windows, removing an open file is not possible and this...

> diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
> index 47b058af7e..694d8e8dc2 100644
> --- a/t/t5411/test-0000-standard-git-push.sh
> +++ b/t/t5411/test-0000-standard-git-push.sh
> @@ -40,6 +40,8 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
>  		-e "/^To / { p; }" \
>  		-e "/^ ! / { p; }" \
>  		<out >actual &&
> +	# Prevent accidential changes by the internal "receive-pack" process.
> +	rm out &&

... would fail.

That said, your next patch removes a lot of uses of the 'out' file
against which this 'rm out' should protect. Doesn't this make this patch
unnecessary?

>  	cat >expect <<-EOF &&
>  	To <URL/of/upstream.git>
>  	 ! [rejected] main -> main (non-fast-forward)

-- Hannes

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH 1/2] t5411: remove file after use to prevent overwriting
  2021-01-18 18:21                                                   ` Johannes Sixt
@ 2021-01-19  0:48                                                     ` Jiang Xin
  2021-01-19 10:24                                                     ` [PATCH v2 0/2] t5411 out file overwrite fix Jiang Xin
                                                                       ` (2 subsequent siblings)
  3 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-01-19  0:48 UTC (permalink / raw)
  To: Johannes Sixt; +Cc: Jiang Xin, Junio C Hamano, Git List, SZEDER Gábor

Johannes Sixt <j6t@kdbg.org> 于2021年1月19日周二 上午2:21写道:
>
> Am 18.01.21 um 14:30 schrieb Jiang Xin:
> > From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> >
> > SZEDER reported that t5411 failed in Travis CI's s390x environment a
> > couple of times, and could be reproduced with '--stress' test on this
> > specific environment.  The test failure messages might look like this:
> >
> >     + test_cmp expect actual
> >     --- expect      2021-01-17 21:55:23.430750004 +0000
> >     +++ actual      2021-01-17 21:55:23.430750004 +0000
> >     @@ -1 +1 @@
> >     -<COMMIT-A> refs/heads/main
> >     +<COMMIT-A> refs/heads/maifatal: the remote end hung up unexpectedly
> >     error: last command exited with $?=1
> >     not ok 86 - proc-receive: not support push options (builtin protocol)
> >
> > The file 'actual' is filtered from the file 'out' which contains result
> > of 'git show-ref' command.  Due to the error messages from other process
> > is written into the file 'out' accidentally, t5411 failed.  SZEDER finds
> > the root cause of this issue:
> >
> >  - 'git push' is executed with its standard output and error redirected
> >    to the file 'out'.
> >
> >  - 'git push' executes 'git receive-pack' internally, which inherits
> >    the open file descriptors, so its output and error goes into that
> >    same 'out' file.
> >
> >  - 'git push' ends without waiting for the close of 'git-receive-pack'
> >    for some cases, and the file 'out' is reused for test of
> >    'git show-ref' afterwards.
> >
> >  - A mixture of the output of 'git show-ref' abd 'git receive-pack'
> >    leads to this issue.
> >
> > To resolve this issue, we can remove the file 'out' after use.  The
> > long-running 'git receive-pack' will not redirect its output to the new
> > created 'out' file which has a different file descriptor.
>
> On Windows, removing an open file is not possible and this...
>
> > diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
> > index 47b058af7e..694d8e8dc2 100644
> > --- a/t/t5411/test-0000-standard-git-push.sh
> > +++ b/t/t5411/test-0000-standard-git-push.sh
> > @@ -40,6 +40,8 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
> >               -e "/^To / { p; }" \
> >               -e "/^ ! / { p; }" \
> >               <out >actual &&
> > +     # Prevent accidential changes by the internal "receive-pack" process.
> > +     rm out &&
>
> ... would fail.

Thank you for reminding. It seems that the only way is to use a
different output file name.

> That said, your next patch removes a lot of uses of the 'out' file
> against which this 'rm out' should protect. Doesn't this make this patch
> unnecessary?

The other test cases also write to the same "out" file, so the issue
still needs to be fixed.

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v2 0/2] t5411 out file overwrite fix
  2021-01-18 18:21                                                   ` Johannes Sixt
  2021-01-19  0:48                                                     ` Jiang Xin
@ 2021-01-19 10:24                                                     ` Jiang Xin
  2021-01-19 10:24                                                     ` [PATCH v2 1/2] t5411: use different out file to prevent overwriting Jiang Xin
  2021-01-19 10:24                                                     ` [PATCH v2 " Jiang Xin
  3 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-01-19 10:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor, Johannes Sixt; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

t5411 has some test cases on the behavior of failed 'git push'
commands.  Even the 'git push' command ended, the output file
may still be updated with messages from 'git-receive-pack' command
running in background.  This breaks test cases which reuse the
same 'out' file.

## Changes since v1

+ Use different 'out' file instead of removing it which does not work on
  Windows.

## Range-diff v1..v2

1:  037e52118d ! 1:  3ec70c1c68 t5411: remove file after use to prevent overwriting
    @@ Metadata
     Author: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## Commit message ##
    -    t5411: remove file after use to prevent overwriting
    +    t5411: use different out file to prevent overwriting
     
         SZEDER reported that t5411 failed in Travis CI's s390x environment a
         couple of times, and could be reproduced with '--stress' test on this
    @@ Commit message
          - A mixture of the output of 'git show-ref' abd 'git receive-pack'
            leads to this issue.
     
    -    To resolve this issue, we can remove the file 'out' after use.  The
    -    long-running 'git receive-pack' will not redirect its output to the new
    -    created 'out' file which has a different file descriptor.
    +    The first intuitive reaction to resolve this issue is to remove the
    +    file 'out' after use, so that the newly created file 'out' will have a
    +    different file descriptor and will not be overwritten by the
    +    'git receive-pack' process.  But Johannes pointed out that removing an
    +    open file is not possible on Windows.  So we use different temporary
    +    file names to store the output of 'git push' to solve this issue.
     
         Reported-by: SZEDER Gábor <szeder.dev@gmail.com>
    +    Helped-by: Johannes Sixt <j6t@kdbg.org>
         Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
     
      ## t/t5411/test-0000-standard-git-push.sh ##
     @@ t/t5411/test-0000-standard-git-push.sh: test_expect_success "git-push --atomic ($PROTOCOL)" '

     ... ...

-- 

Jiang Xin (2):
  t5411: use different out file to prevent overwriting
  t5411: refactor check of refs using test_cmp_refs

 t/t5411/common-functions.sh                   | 15 +++
 t/t5411/once-0010-report-status-v1.sh         |  5 +-
 t/t5411/test-0000-standard-git-push.sh        | 32 +++----
 .../test-0001-standard-git-push--porcelain.sh | 32 +++----
 t/t5411/test-0002-pre-receive-declined.sh     | 10 +-
 ...st-0003-pre-receive-declined--porcelain.sh | 10 +-
 t/t5411/test-0011-no-hook-error.sh            | 20 ++--
 t/t5411/test-0012-no-hook-error--porcelain.sh | 20 ++--
 t/t5411/test-0013-bad-protocol.sh             | 95 +++++++------------
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 95 +++++++------------
 t/t5411/test-0020-report-ng.sh                | 20 ++--
 t/t5411/test-0021-report-ng--porcelain.sh     | 20 ++--
 t/t5411/test-0022-report-unexpect-ref.sh      | 10 +-
 ...est-0023-report-unexpect-ref--porcelain.sh | 10 +-
 t/t5411/test-0024-report-unknown-ref.sh       | 10 +-
 ...test-0025-report-unknown-ref--porcelain.sh | 10 +-
 t/t5411/test-0026-push-options.sh             | 22 ++---
 t/t5411/test-0027-push-options--porcelain.sh  | 22 ++---
 t/t5411/test-0030-report-ok.sh                |  6 +-
 t/t5411/test-0031-report-ok--porcelain.sh     |  6 +-
 t/t5411/test-0032-report-with-options.sh      |  9 +-
 ...est-0033-report-with-options--porcelain.sh |  9 +-
 t/t5411/test-0034-report-ft.sh                |  6 +-
 t/t5411/test-0035-report-ft--porcelain.sh     |  6 +-
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 18 ++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 18 ++--
 t/t5411/test-0038-report-mixed-refs.sh        | 10 +-
 .../test-0039-report-mixed-refs--porcelain.sh | 10 +-
 t/t5411/test-0040-process-all-refs.sh         |  6 +-
 .../test-0041-process-all-refs--porcelain.sh  |  6 +-
 ...t-0050-proc-receive-refs-with-modifiers.sh | 18 ++--
 31 files changed, 224 insertions(+), 362 deletions(-)

-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v2 1/2] t5411: use different out file to prevent overwriting
  2021-01-18 18:21                                                   ` Johannes Sixt
  2021-01-19  0:48                                                     ` Jiang Xin
  2021-01-19 10:24                                                     ` [PATCH v2 0/2] t5411 out file overwrite fix Jiang Xin
@ 2021-01-19 10:24                                                     ` Jiang Xin
  2021-01-20 12:49                                                       ` SZEDER Gábor
  2021-01-19 10:24                                                     ` [PATCH v2 " Jiang Xin
  3 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2021-01-19 10:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor, Johannes Sixt; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

SZEDER reported that t5411 failed in Travis CI's s390x environment a
couple of times, and could be reproduced with '--stress' test on this
specific environment.  The test failure messages might look like this:

    + test_cmp expect actual
    --- expect      2021-01-17 21:55:23.430750004 +0000
    +++ actual      2021-01-17 21:55:23.430750004 +0000
    @@ -1 +1 @@
    -<COMMIT-A> refs/heads/main
    +<COMMIT-A> refs/heads/maifatal: the remote end hung up unexpectedly
    error: last command exited with $?=1
    not ok 86 - proc-receive: not support push options (builtin protocol)

The file 'actual' is filtered from the file 'out' which contains result
of 'git show-ref' command.  Due to the error messages from other process
is written into the file 'out' accidentally, t5411 failed.  SZEDER finds
the root cause of this issue:

 - 'git push' is executed with its standard output and error redirected
   to the file 'out'.

 - 'git push' executes 'git receive-pack' internally, which inherits
   the open file descriptors, so its output and error goes into that
   same 'out' file.

 - 'git push' ends without waiting for the close of 'git-receive-pack'
   for some cases, and the file 'out' is reused for test of
   'git show-ref' afterwards.

 - A mixture of the output of 'git show-ref' abd 'git receive-pack'
   leads to this issue.

The first intuitive reaction to resolve this issue is to remove the
file 'out' after use, so that the newly created file 'out' will have a
different file descriptor and will not be overwritten by the
'git receive-pack' process.  But Johannes pointed out that removing an
open file is not possible on Windows.  So we use different temporary
file names to store the output of 'git push' to solve this issue.

Reported-by: SZEDER Gábor <szeder.dev@gmail.com>
Helped-by: Johannes Sixt <j6t@kdbg.org>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/test-0000-standard-git-push.sh        |  8 +--
 .../test-0001-standard-git-push--porcelain.sh |  8 +--
 t/t5411/test-0002-pre-receive-declined.sh     |  4 +-
 ...st-0003-pre-receive-declined--porcelain.sh |  4 +-
 t/t5411/test-0011-no-hook-error.sh            |  8 +--
 t/t5411/test-0012-no-hook-error--porcelain.sh |  8 +--
 t/t5411/test-0013-bad-protocol.sh             | 50 +++++++++----------
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 50 +++++++++----------
 t/t5411/test-0020-report-ng.sh                |  8 +--
 t/t5411/test-0021-report-ng--porcelain.sh     |  8 +--
 t/t5411/test-0022-report-unexpect-ref.sh      |  4 +-
 ...est-0023-report-unexpect-ref--porcelain.sh |  4 +-
 t/t5411/test-0024-report-unknown-ref.sh       |  4 +-
 ...test-0025-report-unknown-ref--porcelain.sh |  4 +-
 t/t5411/test-0026-push-options.sh             |  4 +-
 t/t5411/test-0027-push-options--porcelain.sh  |  4 +-
 t/t5411/test-0032-report-with-options.sh      |  4 +-
 ...est-0033-report-with-options--porcelain.sh |  4 +-
 t/t5411/test-0038-report-mixed-refs.sh        |  4 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  4 +-
 20 files changed, 98 insertions(+), 98 deletions(-)

diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index 47b058af7e..b074417d4b 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -35,11 +35,11 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 	test_must_fail git -C workbench push --atomic origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
+		>out-0000-1 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-0000-1 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [rejected] main -> main (non-fast-forward)
@@ -65,8 +65,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
 		push origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0000-2 2>&1 &&
+	make_user_friendly_and_stable_output <out-0000-2 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index bbead12bbb..8a0de5cac8 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -36,12 +36,12 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --atomic --porcelain origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
+		>out-0001-1 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "s/^# GETTEXT POISON #//" \
 		-e "/^To / { p; }" \
 		-e "/^! / { p; }" \
-		<out >actual &&
+		<out-0001-1 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    refs/heads/main:refs/heads/main    [rejected] (non-fast-forward)
@@ -67,8 +67,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
 		push --porcelain origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0001-2 2>&1 &&
+	make_user_friendly_and_stable_output <out-0001-2 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
index e7d113a158..83edcb037b 100644
--- a/t/t5411/test-0002-pre-receive-declined.sh
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -12,8 +12,8 @@ test_expect_success "git-push is declined ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		$B:refs/heads/main \
 		HEAD:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0002-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0002-1 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] <COMMIT-B> -> main (pre-receive hook declined)
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
index cc0cca6a47..8f3bbeccca 100644
--- a/t/t5411/test-0003-pre-receive-declined--porcelain.sh
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -12,8 +12,8 @@ test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		$B:refs/heads/main \
 		HEAD:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0003-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0003-1 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    <COMMIT-B>:refs/heads/main    [remote rejected] (pre-receive hook declined)
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
index c50830982f..11c85a32d5 100644
--- a/t/t5411/test-0011-no-hook-error.sh
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -5,8 +5,8 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)
 	test_must_fail git -C workbench push origin \
 		HEAD:next \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0011-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0011-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -41,8 +41,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	test_must_fail git -C workbench push --atomic origin \
 		$B:main \
 		HEAD:next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-0011-2 2>&1 &&
+	make_user_friendly_and_stable_output <out-0011-2 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
index 14ea433481..f89754b480 100644
--- a/t/t5411/test-0012-no-hook-error--porcelain.sh
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -5,8 +5,8 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:next \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0012-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0012-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -42,8 +42,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	test_must_fail git -C workbench push --porcelain --atomic origin \
 		$B:main \
 		HEAD:next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-0012-2 2>&1 &&
+	make_user_friendly_and_stable_output <out-0012-2 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index b9be12be77..888fd951c3 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -11,8 +11,8 @@ test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0013-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0013-1 >actual &&
 
 	# Check status report for git-push
 	sed -n \
@@ -55,18 +55,18 @@ test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0013-2 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-0013-2 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-read-version option" out-0013-2 &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-0013-2 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -89,18 +89,18 @@ test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0013-3 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-0013-3 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-write-version option" out-0013-3 &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-0013-3 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -123,17 +123,17 @@ test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0013-4 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-0013-4 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-commands option" out &&
+	grep "remote: fatal: die with the --die-read-commands option" out-0013-4 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -158,17 +158,17 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_must_fail git -C workbench push origin \
 		-o reviewers=user1,user2 \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0013-5 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-0013-5 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-push-options option" out &&
+	grep "remote: fatal: die with the --die-read-push-options option" out-0013-5 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -191,17 +191,17 @@ test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0013-6 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-0013-6 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-report option" out &&
+	grep "remote: fatal: die with the --die-write-report option" out-0013-6 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -224,8 +224,8 @@ test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/heads/next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-0013-7 2>&1 &&
+	make_user_friendly_and_stable_output <out-0013-7 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -270,8 +270,8 @@ test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic\
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0013-8 2>&1 &&
+	make_user_friendly_and_stable_output <out-0013-8 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -306,8 +306,8 @@ test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 			HEAD:refs/for/main/topic \
-			>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+			>out-0013-9 2>&1 &&
+	make_user_friendly_and_stable_output <out-0013-9 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index fdb4569109..1ecf0a3976 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -11,8 +11,8 @@ test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL/porcela
 test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0014-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0014-1 >actual &&
 
 	# Check status report for git-push
 	sed -n \
@@ -55,18 +55,18 @@ test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0014-2 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-0014-2 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-read-version option" out-0014-2 &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-0014-2 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -89,18 +89,18 @@ test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0014-3 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-0014-3 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-write-version option" out-0014-3 &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-0014-3 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -123,17 +123,17 @@ test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0014-4 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-0014-4 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-commands option" out &&
+	grep "remote: fatal: die with the --die-read-commands option" out-0014-4 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -158,17 +158,17 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_must_fail git -C workbench push --porcelain origin \
 		-o reviewers=user1,user2 \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0014-5 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-0014-5 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-push-options option" out &&
+	grep "remote: fatal: die with the --die-read-push-options option" out-0014-5 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -191,17 +191,17 @@ test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-0014-6 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-0014-6 >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-report option" out &&
+	grep "remote: fatal: die with the --die-write-report option" out-0014-6 &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -224,8 +224,8 @@ test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
 test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/heads/next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-0014-7 2>&1 &&
+	make_user_friendly_and_stable_output <out-0014-7 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -270,8 +270,8 @@ test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
 test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic\
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0014-8 2>&1 &&
+	make_user_friendly_and_stable_output <out-0014-8 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -307,8 +307,8 @@ test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelai
 test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 			HEAD:refs/for/main/topic \
-			>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+			>out-0014-9 2>&1 &&
+	make_user_friendly_and_stable_output <out-0014-9 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
index 5a9e0daf2d..41efafc55e 100644
--- a/t/t5411/test-0020-report-ng.sh
+++ b/t/t5411/test-0020-report-ng.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
 test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0020-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0020-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -46,8 +46,8 @@ test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
 test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0020-2 2>&1 &&
+	make_user_friendly_and_stable_output <out-0020-2 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
index 93475a83cf..65b22618cc 100644
--- a/t/t5411/test-0021-report-ng--porcelain.sh
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL/porcelai
 test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0021-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0021-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -47,8 +47,8 @@ test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)"
 test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0021-2 2>&1 &&
+	make_user_friendly_and_stable_output <out-0021-2 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
index f8be8a0ba1..4cbd474678 100644
--- a/t/t5411/test-0022-report-unexpect-ref.sh
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -13,8 +13,8 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		$B:refs/heads/main \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0022-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0022-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
index 778150fa03..7804b87721 100644
--- a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -13,8 +13,8 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)"
 	test_must_fail git -C workbench push --porcelain origin \
 		$B:refs/heads/main \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0023-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0023-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
index d4e74e4681..a048cb0f02 100644
--- a/t/t5411/test-0024-report-unknown-ref.sh
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
 test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/a/b/c/my/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0024-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0024-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
index 039e8b6163..b3c4fe6330 100644
--- a/t/t5411/test-0025-report-unknown-ref--porcelain.sh
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelai
 test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/a/b/c/my/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0025-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0025-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index e88edb16a4..7bc627cee6 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -16,8 +16,8 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
 		-o reviewer=user1 \
 		origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0026-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0026-1 >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index 3a6561b5ea..5b4884a260 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -17,8 +17,8 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
 		-o reviewer=user1 \
 		origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0027-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0027-1 >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a0faf5c7ff..f5c67a1cc3 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -13,8 +13,8 @@ test_expect_success "setup proc-receive hook (option without matching ok, $PROTO
 test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0032-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0032-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 32ae26bcfb..35e785a0fb 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -13,8 +13,8 @@ test_expect_success "setup proc-receive hook (option without matching ok, $PROTO
 test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0033-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0033-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 0d071ebaa6..0c31447aaa 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -24,8 +24,8 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 		HEAD:refs/heads/foo \
 		HEAD:refs/for/main/topic \
 		HEAD:refs/for/next/topic3 \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0038-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0038-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index d8409912fd..7007dd98fc 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -24,8 +24,8 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 		HEAD:refs/heads/foo \
 		HEAD:refs/for/main/topic \
 		HEAD:refs/for/next/topic3 \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-0039-1 2>&1 &&
+	make_user_friendly_and_stable_output <out-0039-1 >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v2 2/2] t5411: refactor check of refs using test_cmp_refs
  2021-01-18 18:21                                                   ` Johannes Sixt
                                                                       ` (2 preceding siblings ...)
  2021-01-19 10:24                                                     ` [PATCH v2 1/2] t5411: use different out file to prevent overwriting Jiang Xin
@ 2021-01-19 10:24                                                     ` Jiang Xin
  3 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-01-19 10:24 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor, Johannes Sixt; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new helper 'test_cmp_refs' to check references in a repository.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/common-functions.sh                   | 15 +++++++
 t/t5411/once-0010-report-status-v1.sh         |  5 +--
 t/t5411/test-0000-standard-git-push.sh        | 24 ++++------
 .../test-0001-standard-git-push--porcelain.sh | 24 ++++------
 t/t5411/test-0002-pre-receive-declined.sh     |  6 +--
 ...st-0003-pre-receive-declined--porcelain.sh |  6 +--
 t/t5411/test-0011-no-hook-error.sh            | 12 ++---
 t/t5411/test-0012-no-hook-error--porcelain.sh | 12 ++---
 t/t5411/test-0013-bad-protocol.sh             | 45 ++++---------------
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 45 ++++---------------
 t/t5411/test-0020-report-ng.sh                | 12 ++---
 t/t5411/test-0021-report-ng--porcelain.sh     | 12 ++---
 t/t5411/test-0022-report-unexpect-ref.sh      |  6 +--
 ...est-0023-report-unexpect-ref--porcelain.sh |  6 +--
 t/t5411/test-0024-report-unknown-ref.sh       |  6 +--
 ...test-0025-report-unknown-ref--porcelain.sh |  6 +--
 t/t5411/test-0026-push-options.sh             | 18 +++-----
 t/t5411/test-0027-push-options--porcelain.sh  | 18 +++-----
 t/t5411/test-0030-report-ok.sh                |  6 +--
 t/t5411/test-0031-report-ok--porcelain.sh     |  6 +--
 t/t5411/test-0032-report-with-options.sh      |  5 +--
 ...est-0033-report-with-options--porcelain.sh |  5 +--
 t/t5411/test-0034-report-ft.sh                |  6 +--
 t/t5411/test-0035-report-ft--porcelain.sh     |  6 +--
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 18 +++-----
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 18 +++-----
 t/t5411/test-0038-report-mixed-refs.sh        |  6 +--
 .../test-0039-report-mixed-refs--porcelain.sh |  6 +--
 t/t5411/test-0040-process-all-refs.sh         |  6 +--
 .../test-0041-process-all-refs--porcelain.sh  |  6 +--
 ...t-0050-proc-receive-refs-with-modifiers.sh | 18 +++-----
 31 files changed, 126 insertions(+), 264 deletions(-)

diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 344d13f61a..4835dd5182 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -59,3 +59,18 @@ filter_out_user_friendly_and_stable_output () {
 	make_user_friendly_and_stable_output |
 		sed -n ${1+"$@"}
 }
+
+test_cmp_refs () {
+	indir=
+	if test "$1" = "-C"
+	then
+		shift
+		indir="$1"
+		shift
+	fi
+	indir=${indir:+"$indir"/}
+	cat >show-ref.expect &&
+	git ${indir:+ -C "$indir"} show-ref >show-ref.pristine &&
+	make_user_friendly_and_stable_output <show-ref.pristine >show-ref.filtered &&
+	test_cmp show-ref.expect show-ref.filtered
+}
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index cb431a9c91..1233a46eac 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -83,12 +83,9 @@ test_expect_success "proc-receive: report status v1" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/for/main/topic1
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index b074417d4b..63e0536727 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -19,13 +19,11 @@ test_expect_success "git-push ($PROTOCOL)" '
 	 * [new branch] HEAD -> next
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -46,13 +44,11 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 	 ! [rejected] <COMMIT-B> -> next (atomic push failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -77,13 +73,11 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
 	 ! [rejected] main -> main (non-fast-forward)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-B> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(B)
@@ -119,15 +113,13 @@ test_expect_success "git-push -f ($PROTOCOL)" '
 	 * [new branch] HEAD -> a/b/c
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/a/b/c
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/review/main/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)  tags/v123  refs/review/main/topic(A)  a/b/c(A)
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index 8a0de5cac8..4b872eb0a6 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -20,13 +20,11 @@ test_expect_success "git-push ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -48,13 +46,11 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 	!    <COMMIT-B>:refs/heads/next    [rejected] (atomic push failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -80,13 +76,11 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-B> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(B)
@@ -123,15 +117,13 @@ test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/a/b/c
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/review/main/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)  tags/v123  refs/review/main/topic(A)  a/b/c(A)
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
index 83edcb037b..ce1c18f824 100644
--- a/t/t5411/test-0002-pre-receive-declined.sh
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -20,12 +20,10 @@ test_expect_success "git-push is declined ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "cleanup ($PROTOCOL)" '
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
index 8f3bbeccca..7f882d9c12 100644
--- a/t/t5411/test-0003-pre-receive-declined--porcelain.sh
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -21,12 +21,10 @@ test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "cleanup ($PROTOCOL/porcelain)" '
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
index 11c85a32d5..3c42995019 100644
--- a/t/t5411/test-0011-no-hook-error.sh
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -19,13 +19,11 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -55,10 +53,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
index f89754b480..b8603607fe 100644
--- a/t/t5411/test-0012-no-hook-error--porcelain.sh
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -20,13 +20,11 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -57,10 +55,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index 888fd951c3..895ba8e3b9 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -34,12 +34,9 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual-error &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL)" '
@@ -68,12 +65,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	grep "remote: fatal: die with the --die-read-version option" out-0013-2 &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-0013-2 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL)" '
@@ -102,12 +96,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	grep "remote: fatal: die with the --die-write-version option" out-0013-3 &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-0013-3 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL)" '
@@ -135,12 +126,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-commands option" out-0013-4 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL)" '
@@ -170,12 +158,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-push-options option" out-0013-5 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL)" '
@@ -203,12 +188,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-report option" out-0013-6 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
@@ -240,13 +222,10 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -284,12 +263,9 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
@@ -320,10 +296,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index 1ecf0a3976..d66b946c4f 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -34,12 +34,9 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porc
 	EOF
 	test_cmp expect actual-error &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL/porcelain)" '
@@ -68,12 +65,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	grep "remote: fatal: die with the --die-read-version option" out-0014-2 &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-0014-2 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL/porcelain)" '
@@ -102,12 +96,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	grep "remote: fatal: die with the --die-write-version option" out-0014-3 &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-0014-3 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL/porcelain)" '
@@ -135,12 +126,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-commands option" out-0014-4 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL/porcelain)" '
@@ -170,12 +158,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-push-options option" out-0014-5 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL/porcelain)" '
@@ -203,12 +188,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-report option" out-0014-6 &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
@@ -241,13 +223,10 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -285,12 +264,9 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelain)" '
@@ -322,10 +298,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porce
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
index 41efafc55e..d3aa071a21 100644
--- a/t/t5411/test-0020-report-ng.sh
+++ b/t/t5411/test-0020-report-ng.sh
@@ -24,12 +24,10 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
@@ -58,10 +56,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)"
 	 ! [remote rejected] HEAD -> refs/for/main/topic (error msg)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
index 65b22618cc..9bb82c2a30 100644
--- a/t/t5411/test-0021-report-ng--porcelain.sh
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -25,12 +25,10 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/por
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)" '
@@ -60,10 +58,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/p
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
index 4cbd474678..405c6b3372 100644
--- a/t/t5411/test-0022-report-unexpect-ref.sh
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -30,12 +30,10 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
index 7804b87721..1fe3120741 100644
--- a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -31,12 +31,10 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)"
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
index a048cb0f02..b868d0bd7c 100644
--- a/t/t5411/test-0024-report-unknown-ref.sh
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -25,10 +25,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
index b3c4fe6330..813b005797 100644
--- a/t/t5411/test-0025-report-unknown-ref--porcelain.sh
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -26,10 +26,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index 7bc627cee6..c8e453f993 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -20,12 +20,10 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
 	make_user_friendly_and_stable_output <out-0026-1 >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "enable push options ($PROTOCOL)" '
@@ -69,13 +67,11 @@ test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL)
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "restore proc-receive hook ($PROTOCOL)" '
@@ -123,13 +119,11 @@ test_expect_success "proc-receive: push with options ($PROTOCOL)" '
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index 5b4884a260..804d666875 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -21,12 +21,10 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
 	make_user_friendly_and_stable_output <out-0027-1 >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "enable push options ($PROTOCOL/porcelain)" '
@@ -72,13 +70,11 @@ test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL/
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "restore proc-receive hook ($PROTOCOL/porcelain)" '
@@ -128,13 +124,11 @@ test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
index 5d6feef118..8acb4f204f 100644
--- a/t/t5411/test-0030-report-ok.sh
+++ b/t/t5411/test-0030-report-ok.sh
@@ -26,10 +26,8 @@ test_expect_success "proc-receive: ok ($PROTOCOL)" '
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
index 91666d32df..a967718046 100644
--- a/t/t5411/test-0031-report-ok--porcelain.sh
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -27,10 +27,8 @@ test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index f5c67a1cc3..6040ea7aa7 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -247,10 +247,7 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 35e785a0fb..6c9b120eaf 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -256,10 +256,7 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
index c355c290d2..6e0d08b327 100644
--- a/t/t5411/test-0034-report-ft.sh
+++ b/t/t5411/test-0034-report-ft.sh
@@ -28,13 +28,11 @@ test_expect_success "proc-receive: fall throught, let receive-pack to execute ($
 	 * [new reference] <COMMIT-B> -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/for/main/topic
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             refs/for/main/topic(A)
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
index 8ce4e58f2a..81bae9f2ec 100644
--- a/t/t5411/test-0035-report-ft--porcelain.sh
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -29,13 +29,11 @@ test_expect_success "proc-receive: fall throught, let receive-pack to execute ($
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/for/main/topic
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             refs/for/main/topic(A)
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index fad8eea8a0..be9b18b2b6 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -65,12 +65,10 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
@@ -142,12 +140,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
@@ -205,12 +201,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index dc254d57eb..95fb89c031 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -51,12 +51,10 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL/porcelain)" '
@@ -114,12 +112,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL/porcelain)" '
@@ -163,10 +159,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 0c31447aaa..1fd5bfd1e2 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -65,15 +65,13 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)             foo(A)  bar(A))  baz(A)
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 7007dd98fc..23fb796efe 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -66,15 +66,13 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)             foo(A)  bar(A))  baz(A)
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
index 2565302a17..fdcdcc7c2e 100644
--- a/t/t5411/test-0040-process-all-refs.sh
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -92,14 +92,12 @@ test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
 	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             bar(A)  baz(B)
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
index e21420b60d..73b35fe0aa 100644
--- a/t/t5411/test-0041-process-all-refs--porcelain.sh
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -93,14 +93,12 @@ test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             bar(A)  baz(B)
diff --git a/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
index 2e29518ec5..7214647ada 100644
--- a/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
+++ b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
@@ -50,12 +50,10 @@ test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
 	 * [new reference] v123 -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)
@@ -63,14 +61,12 @@ test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
 test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
 	git -C "$upstream" update-ref refs/heads/topic $A &&
 	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook ($PROTOCOL)" '
@@ -125,11 +121,9 @@ test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOC
 	 * [new reference] <COMMIT-A> -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-B> refs/heads/topic
 	EOF
-	test_cmp expect actual
 '
-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2021-01-18  8:24                                                 ` Jiang Xin
@ 2021-01-20 12:28                                                   ` SZEDER Gábor
  2021-01-21  2:21                                                     ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: SZEDER Gábor @ 2021-01-20 12:28 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Git List, Jiang Xin

On Mon, Jan 18, 2021 at 04:24:11PM +0800, Jiang Xin wrote:
> SZEDER Gábor <szeder.dev@gmail.com> 于2021年1月18日周一 上午6:21写道:
> >
> >
> > This patch added a whole lot of test cases, and two of them '86 -
> > proc-receive: not support push options (builtin protocol)' and '95 -
> > proc-receive: not support push options (builtin protocol/porcelain)'
> > are prone to rare failures.
> >
> > On Thu, Aug 27, 2020 at 11:45:44AM -0400, Jiang Xin wrote:
> > > diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
> > > new file mode 100644
> > > index 0000000000..d0c4da8b23
> > > --- /dev/null
> > > +++ b/t/t5411/test-0026-push-options.sh
> >
> > > +# Refs of upstream : master(A)
> > > +# Refs of workbench: master(A)  tags/v123
> > > +# git push -o ...  :                       refs/for/master/topic
> > > +test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
> > > +     test_must_fail git -C workbench push \
> > > +             -o issue=123 \
> > > +             -o reviewer=user1 \
> > > +             origin \
> > > +             HEAD:refs/for/master/topic \
> > > +             >out 2>&1 &&
> >
> > Three relevant things are happening here:
> >
> >   - 'git push' is executed with its standard output and error
> >     redirected to the file 'out'.
> >
> >   - 'git push' executes 'git receive-pack' internally, which inherits
> >     the open file descriptors, so its output and error goes into that
> >     same 'out' file.
> >
> >   - 'git push' is expected to fail when it finds out that the other
> >     side doesn't support push options, but it does so with a simple
> >     die() right away, without waiting for its child 'git receive-pack'
> >     process to finish.
> >
> > > +     make_user_friendly_and_stable_output <out >actual &&
> > > +     test_i18ngrep "fatal: the receiving end does not support push options" \
> > > +             actual &&
> > > +     git -C "$upstream" show-ref >out &&
> >
> > Here the shell opens and truncates the file 'out' to write 'git
> > show-ref's output, i.e. it is still the same 'out' file that was used
> > earlier.
> >
> > Consequently, it is possible that 'git receive-pack' is still running,
> > its open file descriptors to 'out' are still valid, and its "fatal:
> > the remote end hung up unexpectedly" error message about the suddenly
> > disappeared 'git push' can partially overwrite the output from 'git
> > show-ref'.


> > I think these are the only two tests that can cause this racy
> > behavior: by instrumenting finish_command() I found that in all other
> > tests where 'git push' is expected to fail it errors out gracefully
> > and waits for its 'git receive-pack' child process.
> 
> Atomic push may have the same problem.

I don't think so, because send_pack() doesn't die() when a ref is
rejected in an atomic push, but returns, and lets its caller terminate
in an usual way, including waiting for 'git receive-pack'.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2 1/2] t5411: use different out file to prevent overwriting
  2021-01-19 10:24                                                     ` [PATCH v2 1/2] t5411: use different out file to prevent overwriting Jiang Xin
@ 2021-01-20 12:49                                                       ` SZEDER Gábor
  2021-01-21  1:59                                                         ` Jiang Xin
                                                                           ` (3 more replies)
  0 siblings, 4 replies; 266+ messages in thread
From: SZEDER Gábor @ 2021-01-20 12:49 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Git List, Johannes Sixt, Jiang Xin

On Tue, Jan 19, 2021 at 05:24:58AM -0500, Jiang Xin wrote:
> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> 
> SZEDER reported that t5411 failed in Travis CI's s390x environment a
> couple of times, and could be reproduced with '--stress' test on this
> specific environment.  The test failure messages might look like this:
> 
>     + test_cmp expect actual
>     --- expect      2021-01-17 21:55:23.430750004 +0000
>     +++ actual      2021-01-17 21:55:23.430750004 +0000
>     @@ -1 +1 @@
>     -<COMMIT-A> refs/heads/main
>     +<COMMIT-A> refs/heads/maifatal: the remote end hung up unexpectedly
>     error: last command exited with $?=1
>     not ok 86 - proc-receive: not support push options (builtin protocol)
> 
> The file 'actual' is filtered from the file 'out' which contains result
> of 'git show-ref' command.  Due to the error messages from other process
> is written into the file 'out' accidentally, t5411 failed.  SZEDER finds
> the root cause of this issue:
> 
>  - 'git push' is executed with its standard output and error redirected
>    to the file 'out'.
> 
>  - 'git push' executes 'git receive-pack' internally, which inherits
>    the open file descriptors, so its output and error goes into that
>    same 'out' file.
> 
>  - 'git push' ends without waiting for the close of 'git-receive-pack'
>    for some cases, and the file 'out' is reused for test of
>    'git show-ref' afterwards.
> 
>  - A mixture of the output of 'git show-ref' abd 'git receive-pack'
>    leads to this issue.
> 
> The first intuitive reaction to resolve this issue is to remove the
> file 'out' after use, so that the newly created file 'out' will have a
> different file descriptor and will not be overwritten by the
> 'git receive-pack' process.  But Johannes pointed out that removing an
> open file is not possible on Windows.  So we use different temporary
> file names to store the output of 'git push' to solve this issue.
> 
> Reported-by: SZEDER Gábor <szeder.dev@gmail.com>
> Helped-by: Johannes Sixt <j6t@kdbg.org>
> Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> ---
>  t/t5411/test-0000-standard-git-push.sh        |  8 +--
>  .../test-0001-standard-git-push--porcelain.sh |  8 +--
>  t/t5411/test-0002-pre-receive-declined.sh     |  4 +-
>  ...st-0003-pre-receive-declined--porcelain.sh |  4 +-
>  t/t5411/test-0011-no-hook-error.sh            |  8 +--
>  t/t5411/test-0012-no-hook-error--porcelain.sh |  8 +--
>  t/t5411/test-0013-bad-protocol.sh             | 50 +++++++++----------
>  t/t5411/test-0014-bad-protocol--porcelain.sh  | 50 +++++++++----------
>  t/t5411/test-0020-report-ng.sh                |  8 +--
>  t/t5411/test-0021-report-ng--porcelain.sh     |  8 +--
>  t/t5411/test-0022-report-unexpect-ref.sh      |  4 +-
>  ...est-0023-report-unexpect-ref--porcelain.sh |  4 +-
>  t/t5411/test-0024-report-unknown-ref.sh       |  4 +-
>  ...test-0025-report-unknown-ref--porcelain.sh |  4 +-
>  t/t5411/test-0026-push-options.sh             |  4 +-
>  t/t5411/test-0027-push-options--porcelain.sh  |  4 +-
>  t/t5411/test-0032-report-with-options.sh      |  4 +-
>  ...est-0033-report-with-options--porcelain.sh |  4 +-
>  t/t5411/test-0038-report-mixed-refs.sh        |  4 +-
>  .../test-0039-report-mixed-refs--porcelain.sh |  4 +-
>  20 files changed, 98 insertions(+), 98 deletions(-)

Wow, this is a lot of churn.

Would it be possible/desirable to die gracefully when the receiving
end doesn't support push options?

> diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
> index 47b058af7e..b074417d4b 100644
> --- a/t/t5411/test-0000-standard-git-push.sh
> +++ b/t/t5411/test-0000-standard-git-push.sh
> @@ -35,11 +35,11 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
>  	test_must_fail git -C workbench push --atomic origin \
>  		main \
>  		$B:refs/heads/next \
> -		>out 2>&1 &&
> +		>out-0000-1 2>&1 &&
>  	filter_out_user_friendly_and_stable_output \
>  		-e "/^To / { p; }" \
>  		-e "/^ ! / { p; }" \
> -		<out >actual &&
> +		<out-0000-1 >actual &&
>  	cat >expect <<-EOF &&
>  	To <URL/of/upstream.git>
>  	 ! [rejected] main -> main (non-fast-forward)
> @@ -65,8 +65,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
>  		push origin \
>  		main \
>  		$B:refs/heads/next \
> -		>out 2>&1 &&
> -	make_user_friendly_and_stable_output <out >actual &&
> +		>out-0000-2 2>&1 &&
> +	make_user_friendly_and_stable_output <out-0000-2 >actual &&

There will be a lot of sequential numbers this way, which might lead
to additional churn in the future, if we ever were to add more tests
somewhere in the middle.  However, our test framework does already
have a counter for test cases, so we could perhaps use that, i.e.
'out-$test_count', to make sure that each test case has its own output
file.


^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v2 1/2] t5411: use different out file to prevent overwriting
  2021-01-20 12:49                                                       ` SZEDER Gábor
@ 2021-01-21  1:59                                                         ` Jiang Xin
  2021-01-21  2:53                                                         ` [PATCH v3 0/2] use unique out file in t5411 Jiang Xin
                                                                           ` (2 subsequent siblings)
  3 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-01-21  1:59 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: Junio C Hamano, Git List, Johannes Sixt, Jiang Xin

SZEDER Gábor <szeder.dev@gmail.com> 于2021年1月20日周三 下午8:49写道:
>
> Would it be possible/desirable to die gracefully when the receiving
> end doesn't support push options?

If want to die gracefully, we need to end the pack protocol by sending
a flush packet, close the connection. However, since we can easily fix
this test case, I think changes for git protocol is unnecessary.

> > diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
> > index 47b058af7e..b074417d4b 100644
> > --- a/t/t5411/test-0000-standard-git-push.sh
> > +++ b/t/t5411/test-0000-standard-git-push.sh
> > @@ -35,11 +35,11 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
> >       test_must_fail git -C workbench push --atomic origin \
> >               main \
> >               $B:refs/heads/next \
> > -             >out 2>&1 &&
> > +             >out-0000-1 2>&1 &&
> >       filter_out_user_friendly_and_stable_output \
> >               -e "/^To / { p; }" \
> >               -e "/^ ! / { p; }" \
> > -             <out >actual &&
> > +             <out-0000-1 >actual &&
> >       cat >expect <<-EOF &&
> >       To <URL/of/upstream.git>
> >        ! [rejected] main -> main (non-fast-forward)
> > @@ -65,8 +65,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
> >               push origin \
> >               main \
> >               $B:refs/heads/next \
> > -             >out 2>&1 &&
> > -     make_user_friendly_and_stable_output <out >actual &&
> > +             >out-0000-2 2>&1 &&
> > +     make_user_friendly_and_stable_output <out-0000-2 >actual &&
>
> There will be a lot of sequential numbers this way, which might lead
> to additional churn in the future, if we ever were to add more tests
> somewhere in the middle.  However, our test framework does already
> have a counter for test cases, so we could perhaps use that, i.e.
> 'out-$test_count', to make sure that each test case has its own output
> file.

Using $test_count is better, and will do.

Thanks.
--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2021-01-20 12:28                                                   ` SZEDER Gábor
@ 2021-01-21  2:21                                                     ` Jiang Xin
  2021-01-21  6:12                                                       ` SZEDER Gábor
  0 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2021-01-21  2:21 UTC (permalink / raw)
  To: SZEDER Gábor; +Cc: Junio C Hamano, Git List, Jiang Xin

SZEDER Gábor <szeder.dev@gmail.com> 于2021年1月20日周三 下午8:29写道:
>
> On Mon, Jan 18, 2021 at 04:24:11PM +0800, Jiang Xin wrote:
> > SZEDER Gábor <szeder.dev@gmail.com> 于2021年1月18日周一 上午6:21写道:
> > >
> > >
> > > This patch added a whole lot of test cases, and two of them '86 -
> > > proc-receive: not support push options (builtin protocol)' and '95 -
> > > proc-receive: not support push options (builtin protocol/porcelain)'
> > > are prone to rare failures.
> > >
> > > On Thu, Aug 27, 2020 at 11:45:44AM -0400, Jiang Xin wrote:
> > > > diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
> > > > new file mode 100644
> > > > index 0000000000..d0c4da8b23
> > > > --- /dev/null
> > > > +++ b/t/t5411/test-0026-push-options.sh
> > >
> > > > +# Refs of upstream : master(A)
> > > > +# Refs of workbench: master(A)  tags/v123
> > > > +# git push -o ...  :                       refs/for/master/topic
> > > > +test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
> > > > +     test_must_fail git -C workbench push \
> > > > +             -o issue=123 \
> > > > +             -o reviewer=user1 \
> > > > +             origin \
> > > > +             HEAD:refs/for/master/topic \
> > > > +             >out 2>&1 &&
> > >
> > > Three relevant things are happening here:
> > >
> > >   - 'git push' is executed with its standard output and error
> > >     redirected to the file 'out'.
> > >
> > >   - 'git push' executes 'git receive-pack' internally, which inherits
> > >     the open file descriptors, so its output and error goes into that
> > >     same 'out' file.
> > >
> > >   - 'git push' is expected to fail when it finds out that the other
> > >     side doesn't support push options, but it does so with a simple
> > >     die() right away, without waiting for its child 'git receive-pack'
> > >     process to finish.
> > >
> > > > +     make_user_friendly_and_stable_output <out >actual &&
> > > > +     test_i18ngrep "fatal: the receiving end does not support push options" \
> > > > +             actual &&
> > > > +     git -C "$upstream" show-ref >out &&
> > >
> > > Here the shell opens and truncates the file 'out' to write 'git
> > > show-ref's output, i.e. it is still the same 'out' file that was used
> > > earlier.
> > >
> > > Consequently, it is possible that 'git receive-pack' is still running,
> > > its open file descriptors to 'out' are still valid, and its "fatal:
> > > the remote end hung up unexpectedly" error message about the suddenly
> > > disappeared 'git push' can partially overwrite the output from 'git
> > > show-ref'.
>
>
> > > I think these are the only two tests that can cause this racy
> > > behavior: by instrumenting finish_command() I found that in all other
> > > tests where 'git push' is expected to fail it errors out gracefully
> > > and waits for its 'git receive-pack' child process.
> >
> > Atomic push may have the same problem.
>
> I don't think so, because send_pack() doesn't die() when a ref is
> rejected in an atomic push, but returns, and lets its caller terminate
> in an usual way, including waiting for 'git receive-pack'.

I find many places where the client side will die() before closing the
service side gracefully:

 + In `transport_push()`, if fail to push a submodule, will die().
 + In `git_transport_push()`, will die() for an unimplemented v2 protocol.
 + In `send_pack()`, will die() if
    the server side has an incompatible hash algorithm, or
    the receiving end does not support --signed push, or
    the receiving end does not support --atomic push, or
    the receiving end does not support push options

^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v3 0/2] use unique out file in t5411
  2021-01-20 12:49                                                       ` SZEDER Gábor
  2021-01-21  1:59                                                         ` Jiang Xin
@ 2021-01-21  2:53                                                         ` Jiang Xin
  2021-02-11 21:52                                                           ` Junio C Hamano
  2021-01-21  2:53                                                         ` [PATCH v3 1/2] t5411: use different out file to prevent overwriting Jiang Xin
  2021-01-21  2:53                                                         ` [PATCH v3 2/2] t5411: refactor check of refs using test_cmp_refs Jiang Xin
  3 siblings, 1 reply; 266+ messages in thread
From: Jiang Xin @ 2021-01-21  2:53 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor, Johannes Sixt; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

t5411 has some test cases for the behavior of failed 'git push'
commands.  Even the 'git push' command ended, the output file
may still be updated with messages from 'git-receive-pack' command
running in background.  This breaks test cases which reuse the
same 'out' file.

## Changes since v2

+ Use unique 'out' file by appending the '-$test_count' suffix to
  prevent accidental overwriting by internal 'git-receive-pack'
  process.

--

Jiang Xin (2):
  t5411: use different out file to prevent overwriting
  t5411: refactor check of refs using test_cmp_refs

 t/t5411/common-functions.sh                   | 15 +++
 t/t5411/once-0010-report-status-v1.sh         |  5 +-
 t/t5411/test-0000-standard-git-push.sh        | 32 +++----
 .../test-0001-standard-git-push--porcelain.sh | 32 +++----
 t/t5411/test-0002-pre-receive-declined.sh     | 10 +-
 ...st-0003-pre-receive-declined--porcelain.sh | 10 +-
 t/t5411/test-0011-no-hook-error.sh            | 20 ++--
 t/t5411/test-0012-no-hook-error--porcelain.sh | 20 ++--
 t/t5411/test-0013-bad-protocol.sh             | 95 +++++++------------
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 95 +++++++------------
 t/t5411/test-0020-report-ng.sh                | 20 ++--
 t/t5411/test-0021-report-ng--porcelain.sh     | 20 ++--
 t/t5411/test-0022-report-unexpect-ref.sh      | 10 +-
 ...est-0023-report-unexpect-ref--porcelain.sh | 10 +-
 t/t5411/test-0024-report-unknown-ref.sh       | 10 +-
 ...test-0025-report-unknown-ref--porcelain.sh | 10 +-
 t/t5411/test-0026-push-options.sh             | 22 ++---
 t/t5411/test-0027-push-options--porcelain.sh  | 22 ++---
 t/t5411/test-0030-report-ok.sh                |  6 +-
 t/t5411/test-0031-report-ok--porcelain.sh     |  6 +-
 t/t5411/test-0032-report-with-options.sh      |  9 +-
 ...est-0033-report-with-options--porcelain.sh |  9 +-
 t/t5411/test-0034-report-ft.sh                |  6 +-
 t/t5411/test-0035-report-ft--porcelain.sh     |  6 +-
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 18 ++--
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 18 ++--
 t/t5411/test-0038-report-mixed-refs.sh        | 10 +-
 .../test-0039-report-mixed-refs--porcelain.sh | 10 +-
 t/t5411/test-0040-process-all-refs.sh         |  6 +-
 .../test-0041-process-all-refs--porcelain.sh  |  6 +-
 ...t-0050-proc-receive-refs-with-modifiers.sh | 18 ++--
 31 files changed, 224 insertions(+), 362 deletions(-)

-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply	[flat|nested] 266+ messages in thread

* [PATCH v3 1/2] t5411: use different out file to prevent overwriting
  2021-01-20 12:49                                                       ` SZEDER Gábor
  2021-01-21  1:59                                                         ` Jiang Xin
  2021-01-21  2:53                                                         ` [PATCH v3 0/2] use unique out file in t5411 Jiang Xin
@ 2021-01-21  2:53                                                         ` Jiang Xin
  2021-01-21  2:53                                                         ` [PATCH v3 2/2] t5411: refactor check of refs using test_cmp_refs Jiang Xin
  3 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-01-21  2:53 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor, Johannes Sixt; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

SZEDER reported that t5411 failed in Travis CI's s390x environment a
couple of times, and could be reproduced with '--stress' test on this
specific environment.  The test failure messages might look like this:

    + test_cmp expect actual
    --- expect      2021-01-17 21:55:23.430750004 +0000
    +++ actual      2021-01-17 21:55:23.430750004 +0000
    @@ -1 +1 @@
    -<COMMIT-A> refs/heads/main
    +<COMMIT-A> refs/heads/maifatal: the remote end hung up unexpectedly
    error: last command exited with $?=1
    not ok 86 - proc-receive: not support push options (builtin protocol)

The file 'actual' is filtered from the file 'out' which contains result
of 'git show-ref' command.  Due to the error messages from other process
is written into the file 'out' accidentally, t5411 failed.  SZEDER finds
the root cause of this issue:

 - 'git push' is executed with its standard output and error redirected
   to the file 'out'.

 - 'git push' executes 'git receive-pack' internally, which inherits
   the open file descriptors, so its output and error goes into that
   same 'out' file.

 - 'git push' ends without waiting for the close of 'git-receive-pack'
   for some cases, and the file 'out' is reused for test of
   'git show-ref' afterwards.

 - A mixture of the output of 'git show-ref' abd 'git receive-pack'
   leads to this issue.

The first intuitive reaction to resolve this issue is to remove the
file 'out' after use, so that the newly created file 'out' will have a
different file descriptor and will not be overwritten by the
'git receive-pack' process.  But Johannes pointed out that removing an
open file is not possible on Windows.  So we use different temporary
file names to store the output of 'git push' to solve this issue.

Reported-by: SZEDER Gábor <szeder.dev@gmail.com>
Helped-by: Johannes Sixt <j6t@kdbg.org>
Helped-by: SZEDER Gábor <szeder.dev@gmail.com>
Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/test-0000-standard-git-push.sh        |  8 +--
 .../test-0001-standard-git-push--porcelain.sh |  8 +--
 t/t5411/test-0002-pre-receive-declined.sh     |  4 +-
 ...st-0003-pre-receive-declined--porcelain.sh |  4 +-
 t/t5411/test-0011-no-hook-error.sh            |  8 +--
 t/t5411/test-0012-no-hook-error--porcelain.sh |  8 +--
 t/t5411/test-0013-bad-protocol.sh             | 50 +++++++++----------
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 50 +++++++++----------
 t/t5411/test-0020-report-ng.sh                |  8 +--
 t/t5411/test-0021-report-ng--porcelain.sh     |  8 +--
 t/t5411/test-0022-report-unexpect-ref.sh      |  4 +-
 ...est-0023-report-unexpect-ref--porcelain.sh |  4 +-
 t/t5411/test-0024-report-unknown-ref.sh       |  4 +-
 ...test-0025-report-unknown-ref--porcelain.sh |  4 +-
 t/t5411/test-0026-push-options.sh             |  4 +-
 t/t5411/test-0027-push-options--porcelain.sh  |  4 +-
 t/t5411/test-0032-report-with-options.sh      |  4 +-
 ...est-0033-report-with-options--porcelain.sh |  4 +-
 t/t5411/test-0038-report-mixed-refs.sh        |  4 +-
 .../test-0039-report-mixed-refs--porcelain.sh |  4 +-
 20 files changed, 98 insertions(+), 98 deletions(-)

diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index 47b058af7e..bdc4907191 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -35,11 +35,11 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 	test_must_fail git -C workbench push --atomic origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [rejected] main -> main (non-fast-forward)
@@ -65,8 +65,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
 		push origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index bbead12bbb..59630ef7e5 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -36,12 +36,12 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --atomic --porcelain origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "s/^# GETTEXT POISON #//" \
 		-e "/^To / { p; }" \
 		-e "/^! / { p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    refs/heads/main:refs/heads/main    [rejected] (non-fast-forward)
@@ -67,8 +67,8 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
 		push --porcelain origin \
 		main \
 		$B:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/next
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
index e7d113a158..c5c76bfe93 100644
--- a/t/t5411/test-0002-pre-receive-declined.sh
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -12,8 +12,8 @@ test_expect_success "git-push is declined ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		$B:refs/heads/main \
 		HEAD:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] <COMMIT-B> -> main (pre-receive hook declined)
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
index cc0cca6a47..45050b642a 100644
--- a/t/t5411/test-0003-pre-receive-declined--porcelain.sh
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -12,8 +12,8 @@ test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		$B:refs/heads/main \
 		HEAD:refs/heads/next \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    <COMMIT-B>:refs/heads/main    [remote rejected] (pre-receive hook declined)
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
index c50830982f..77b45b0cbd 100644
--- a/t/t5411/test-0011-no-hook-error.sh
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -5,8 +5,8 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)
 	test_must_fail git -C workbench push origin \
 		HEAD:next \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -41,8 +41,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	test_must_fail git -C workbench push --atomic origin \
 		$B:main \
 		HEAD:next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
index 14ea433481..142f4835e4 100644
--- a/t/t5411/test-0012-no-hook-error--porcelain.sh
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -5,8 +5,8 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:next \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -42,8 +42,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	test_must_fail git -C workbench push --porcelain --atomic origin \
 		$B:main \
 		HEAD:next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index b9be12be77..bddface163 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -11,8 +11,8 @@ test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 
 	# Check status report for git-push
 	sed -n \
@@ -55,18 +55,18 @@ test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -89,18 +89,18 @@ test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -123,17 +123,17 @@ test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-commands option" out &&
+	grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -158,17 +158,17 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_must_fail git -C workbench push origin \
 		-o reviewers=user1,user2 \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-push-options option" out &&
+	grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -191,17 +191,17 @@ test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; }" \
 		-e "/^ ! / { p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-report option" out &&
+	grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -224,8 +224,8 @@ test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/heads/next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -270,8 +270,8 @@ test_expect_success "setup proc-receive hook (no ref, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic\
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -306,8 +306,8 @@ test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
 test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 			HEAD:refs/for/main/topic \
-			>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+			>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index fdb4569109..64bfa27dcc 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -11,8 +11,8 @@ test_expect_success "setup proc-receive hook (unknown version, $PROTOCOL/porcela
 test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 
 	# Check status report for git-push
 	sed -n \
@@ -55,18 +55,18 @@ test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -89,18 +89,18 @@ test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-version option" out &&
-	grep "remote: error: fail to negotiate version with proc-receive hook" out &&
+	grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
+	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -123,17 +123,17 @@ test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCO
 test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-commands option" out &&
+	grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -158,17 +158,17 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_must_fail git -C workbench push --porcelain origin \
 		-o reviewers=user1,user2 \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-read-push-options option" out &&
+	grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -191,17 +191,17 @@ test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL
 test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
+		>out-$test_count 2>&1 &&
 	filter_out_user_friendly_and_stable_output \
 		-e "/^To / { p; n; p; n; p; }" \
-		<out >actual &&
+		<out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	To <URL/of/upstream.git>
 	!    HEAD:refs/for/main/topic    [remote rejected] (fail to run proc-receive hook)
 	Done
 	EOF
 	test_cmp expect actual &&
-	grep "remote: fatal: die with the --die-write-report option" out &&
+	grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
 
 	git -C "$upstream" show-ref >out &&
 	make_user_friendly_and_stable_output <out >actual &&
@@ -224,8 +224,8 @@ test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
 test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/heads/next \
-		HEAD:refs/for/main/topic >out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		HEAD:refs/for/main/topic >out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
@@ -270,8 +270,8 @@ test_expect_success "setup proc-receive hook (no ref, $PROTOCOL/porcelain)" '
 test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic\
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -307,8 +307,8 @@ test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelai
 test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 			HEAD:refs/for/main/topic \
-			>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+			>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
index 5a9e0daf2d..6618c91841 100644
--- a/t/t5411/test-0020-report-ng.sh
+++ b/t/t5411/test-0020-report-ng.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL)" '
 test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -46,8 +46,8 @@ test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
 test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
index 93475a83cf..44ad1c7de4 100644
--- a/t/t5411/test-0021-report-ng--porcelain.sh
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (ng, no message, $PROTOCOL/porcelai
 test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
@@ -47,8 +47,8 @@ test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)"
 test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
index f8be8a0ba1..b0c5a6e168 100644
--- a/t/t5411/test-0022-report-unexpect-ref.sh
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -13,8 +13,8 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		$B:refs/heads/main \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
index 778150fa03..212927b8dc 100644
--- a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -13,8 +13,8 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)"
 	test_must_fail git -C workbench push --porcelain origin \
 		$B:refs/heads/main \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
index d4e74e4681..94ffad55f2 100644
--- a/t/t5411/test-0024-report-unknown-ref.sh
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL)" '
 test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/a/b/c/my/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
index 039e8b6163..8ee7e66472 100644
--- a/t/t5411/test-0025-report-unknown-ref--porcelain.sh
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -12,8 +12,8 @@ test_expect_success "setup proc-receive hook (unexpected ref, $PROTOCOL/porcelai
 test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/a/b/c/my/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/a/b/c/my/topic
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index e88edb16a4..bb2cf59bdc 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -16,8 +16,8 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
 		-o reviewer=user1 \
 		origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index 3a6561b5ea..54e4f8f1d2 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -17,8 +17,8 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
 		-o reviewer=user1 \
 		origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
 	git -C "$upstream" show-ref >out &&
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index a0faf5c7ff..e6aa0f7018 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -13,8 +13,8 @@ test_expect_success "setup proc-receive hook (option without matching ok, $PROTO
 test_expect_success "proc-receive: report option without matching ok ($PROTOCOL)" '
 	test_must_fail git -C workbench push origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index 32ae26bcfb..a8ca5dd0dd 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -13,8 +13,8 @@ test_expect_success "setup proc-receive hook (option without matching ok, $PROTO
 test_expect_success "proc-receive: report option without matching ok ($PROTOCOL/porcelain)" '
 	test_must_fail git -C workbench push --porcelain origin \
 		HEAD:refs/for/main/topic \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 0d071ebaa6..286f80d423 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -24,8 +24,8 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 		HEAD:refs/heads/foo \
 		HEAD:refs/for/main/topic \
 		HEAD:refs/for/next/topic3 \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index d8409912fd..939b8a3b4f 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -24,8 +24,8 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 		HEAD:refs/heads/foo \
 		HEAD:refs/for/main/topic \
 		HEAD:refs/for/next/topic3 \
-		>out 2>&1 &&
-	make_user_friendly_and_stable_output <out >actual &&
+		>out-$test_count 2>&1 &&
+	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	cat >expect <<-EOF &&
 	remote: # pre-receive hook
 	remote: pre-receive< <COMMIT-A> <COMMIT-B> refs/heads/main
-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* [PATCH v3 2/2] t5411: refactor check of refs using test_cmp_refs
  2021-01-20 12:49                                                       ` SZEDER Gábor
                                                                           ` (2 preceding siblings ...)
  2021-01-21  2:53                                                         ` [PATCH v3 1/2] t5411: use different out file to prevent overwriting Jiang Xin
@ 2021-01-21  2:53                                                         ` Jiang Xin
  3 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-01-21  2:53 UTC (permalink / raw)
  To: Junio C Hamano, Git List, SZEDER Gábor, Johannes Sixt; +Cc: Jiang Xin

From: Jiang Xin <zhiyou.jx@alibaba-inc.com>

Add new helper 'test_cmp_refs' to check references in a repository.

Signed-off-by: Jiang Xin <zhiyou.jx@alibaba-inc.com>
---
 t/t5411/common-functions.sh                   | 15 +++++++
 t/t5411/once-0010-report-status-v1.sh         |  5 +--
 t/t5411/test-0000-standard-git-push.sh        | 24 ++++------
 .../test-0001-standard-git-push--porcelain.sh | 24 ++++------
 t/t5411/test-0002-pre-receive-declined.sh     |  6 +--
 ...st-0003-pre-receive-declined--porcelain.sh |  6 +--
 t/t5411/test-0011-no-hook-error.sh            | 12 ++---
 t/t5411/test-0012-no-hook-error--porcelain.sh | 12 ++---
 t/t5411/test-0013-bad-protocol.sh             | 45 ++++---------------
 t/t5411/test-0014-bad-protocol--porcelain.sh  | 45 ++++---------------
 t/t5411/test-0020-report-ng.sh                | 12 ++---
 t/t5411/test-0021-report-ng--porcelain.sh     | 12 ++---
 t/t5411/test-0022-report-unexpect-ref.sh      |  6 +--
 ...est-0023-report-unexpect-ref--porcelain.sh |  6 +--
 t/t5411/test-0024-report-unknown-ref.sh       |  6 +--
 ...test-0025-report-unknown-ref--porcelain.sh |  6 +--
 t/t5411/test-0026-push-options.sh             | 18 +++-----
 t/t5411/test-0027-push-options--porcelain.sh  | 18 +++-----
 t/t5411/test-0030-report-ok.sh                |  6 +--
 t/t5411/test-0031-report-ok--porcelain.sh     |  6 +--
 t/t5411/test-0032-report-with-options.sh      |  5 +--
 ...est-0033-report-with-options--porcelain.sh |  5 +--
 t/t5411/test-0034-report-ft.sh                |  6 +--
 t/t5411/test-0035-report-ft--porcelain.sh     |  6 +--
 ...t-0036-report-multi-rewrite-for-one-ref.sh | 18 +++-----
 ...rt-multi-rewrite-for-one-ref--porcelain.sh | 18 +++-----
 t/t5411/test-0038-report-mixed-refs.sh        |  6 +--
 .../test-0039-report-mixed-refs--porcelain.sh |  6 +--
 t/t5411/test-0040-process-all-refs.sh         |  6 +--
 .../test-0041-process-all-refs--porcelain.sh  |  6 +--
 ...t-0050-proc-receive-refs-with-modifiers.sh | 18 +++-----
 31 files changed, 126 insertions(+), 264 deletions(-)

diff --git a/t/t5411/common-functions.sh b/t/t5411/common-functions.sh
index 344d13f61a..4835dd5182 100644
--- a/t/t5411/common-functions.sh
+++ b/t/t5411/common-functions.sh
@@ -59,3 +59,18 @@ filter_out_user_friendly_and_stable_output () {
 	make_user_friendly_and_stable_output |
 		sed -n ${1+"$@"}
 }
+
+test_cmp_refs () {
+	indir=
+	if test "$1" = "-C"
+	then
+		shift
+		indir="$1"
+		shift
+	fi
+	indir=${indir:+"$indir"/}
+	cat >show-ref.expect &&
+	git ${indir:+ -C "$indir"} show-ref >show-ref.pristine &&
+	make_user_friendly_and_stable_output <show-ref.pristine >show-ref.filtered &&
+	test_cmp show-ref.expect show-ref.filtered
+}
diff --git a/t/t5411/once-0010-report-status-v1.sh b/t/t5411/once-0010-report-status-v1.sh
index cb431a9c91..1233a46eac 100644
--- a/t/t5411/once-0010-report-status-v1.sh
+++ b/t/t5411/once-0010-report-status-v1.sh
@@ -83,12 +83,9 @@ test_expect_success "proc-receive: report status v1" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/for/main/topic1
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0000-standard-git-push.sh b/t/t5411/test-0000-standard-git-push.sh
index bdc4907191..e1e0175c12 100644
--- a/t/t5411/test-0000-standard-git-push.sh
+++ b/t/t5411/test-0000-standard-git-push.sh
@@ -19,13 +19,11 @@ test_expect_success "git-push ($PROTOCOL)" '
 	 * [new branch] HEAD -> next
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -46,13 +44,11 @@ test_expect_success "git-push --atomic ($PROTOCOL)" '
 	 ! [rejected] <COMMIT-B> -> next (atomic push failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -77,13 +73,11 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL)" '
 	 ! [rejected] main -> main (non-fast-forward)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-B> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(B)
@@ -119,15 +113,13 @@ test_expect_success "git-push -f ($PROTOCOL)" '
 	 * [new branch] HEAD -> a/b/c
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/a/b/c
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/review/main/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)  tags/v123  refs/review/main/topic(A)  a/b/c(A)
diff --git a/t/t5411/test-0001-standard-git-push--porcelain.sh b/t/t5411/test-0001-standard-git-push--porcelain.sh
index 59630ef7e5..bcbda72341 100644
--- a/t/t5411/test-0001-standard-git-push--porcelain.sh
+++ b/t/t5411/test-0001-standard-git-push--porcelain.sh
@@ -20,13 +20,11 @@ test_expect_success "git-push ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -48,13 +46,11 @@ test_expect_success "git-push --atomic ($PROTOCOL/porcelain)" '
 	!    <COMMIT-B>:refs/heads/next    [rejected] (atomic push failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(A)
@@ -80,13 +76,11 @@ test_expect_success "non-fast-forward git-push ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	<COMMIT-B> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)  next(B)
@@ -123,15 +117,13 @@ test_expect_success "git-push -f ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/a/b/c
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/review/main/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)  tags/v123  refs/review/main/topic(A)  a/b/c(A)
diff --git a/t/t5411/test-0002-pre-receive-declined.sh b/t/t5411/test-0002-pre-receive-declined.sh
index c5c76bfe93..0c3490c9b1 100644
--- a/t/t5411/test-0002-pre-receive-declined.sh
+++ b/t/t5411/test-0002-pre-receive-declined.sh
@@ -20,12 +20,10 @@ test_expect_success "git-push is declined ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> next (pre-receive hook declined)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "cleanup ($PROTOCOL)" '
diff --git a/t/t5411/test-0003-pre-receive-declined--porcelain.sh b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
index 45050b642a..e9c9db5d1f 100644
--- a/t/t5411/test-0003-pre-receive-declined--porcelain.sh
+++ b/t/t5411/test-0003-pre-receive-declined--porcelain.sh
@@ -21,12 +21,10 @@ test_expect_success "git-push is declined ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "cleanup ($PROTOCOL/porcelain)" '
diff --git a/t/t5411/test-0011-no-hook-error.sh b/t/t5411/test-0011-no-hook-error.sh
index 77b45b0cbd..3ef136e6ef 100644
--- a/t/t5411/test-0011-no-hook-error.sh
+++ b/t/t5411/test-0011-no-hook-error.sh
@@ -19,13 +19,11 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL)
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -55,10 +53,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0012-no-hook-error--porcelain.sh b/t/t5411/test-0012-no-hook-error--porcelain.sh
index 142f4835e4..19f66fbd7d 100644
--- a/t/t5411/test-0012-no-hook-error--porcelain.sh
+++ b/t/t5411/test-0012-no-hook-error--porcelain.sh
@@ -20,13 +20,11 @@ test_expect_success "proc-receive: no hook, fail to push special ref ($PROTOCOL/
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -57,10 +55,8 @@ test_expect_success "proc-receive: no hook, all failed for atomic push ($PROTOCO
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0013-bad-protocol.sh b/t/t5411/test-0013-bad-protocol.sh
index bddface163..095e613f6f 100644
--- a/t/t5411/test-0013-bad-protocol.sh
+++ b/t/t5411/test-0013-bad-protocol.sh
@@ -34,12 +34,9 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual-error &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL)" '
@@ -68,12 +65,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL)" '
@@ -102,12 +96,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL)" '
@@ -135,12 +126,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL)" '
@@ -170,12 +158,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL)" '
@@ -203,12 +188,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL)" '
@@ -240,13 +222,10 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -284,12 +263,9 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL)" '
@@ -320,10 +296,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0014-bad-protocol--porcelain.sh b/t/t5411/test-0014-bad-protocol--porcelain.sh
index 64bfa27dcc..a44649789c 100644
--- a/t/t5411/test-0014-bad-protocol--porcelain.sh
+++ b/t/t5411/test-0014-bad-protocol--porcelain.sh
@@ -34,12 +34,9 @@ test_expect_success "proc-receive: bad protocol (unknown version, $PROTOCOL/porc
 	EOF
 	test_cmp expect actual-error &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-version, $PROTOCOL/porcelain)" '
@@ -68,12 +65,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
 	grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-version, $PROTOCOL/porcelain)" '
@@ -102,12 +96,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
 	grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
 	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-commands, $PROTOCOL/porcelain)" '
@@ -135,12 +126,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-read-push-options, $PROTOCOL/porcelain)" '
@@ -170,12 +158,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (hook --die-write-report, $PROTOCOL/porcelain)" '
@@ -203,12 +188,9 @@ test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
 	test_cmp expect actual &&
 	grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (no report, $PROTOCOL/porcelain)" '
@@ -241,13 +223,10 @@ test_expect_success "proc-receive: bad protocol (no report, $PROTOCOL/porcelain)
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
@@ -285,12 +264,9 @@ test_expect_success "proc-receive: bad protocol (no ref, $PROTOCOL/porcelain)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (unknown status, $PROTOCOL/porcelain)" '
@@ -322,10 +298,7 @@ test_expect_success "proc-receive: bad protocol (unknown status, $PROTOCOL/porce
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0020-report-ng.sh b/t/t5411/test-0020-report-ng.sh
index 6618c91841..ad2c8f6535 100644
--- a/t/t5411/test-0020-report-ng.sh
+++ b/t/t5411/test-0020-report-ng.sh
@@ -24,12 +24,10 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (failed)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (ng message, $PROTOCOL)" '
@@ -58,10 +56,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL)"
 	 ! [remote rejected] HEAD -> refs/for/main/topic (error msg)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0021-report-ng--porcelain.sh b/t/t5411/test-0021-report-ng--porcelain.sh
index 44ad1c7de4..d8ae9d3414 100644
--- a/t/t5411/test-0021-report-ng--porcelain.sh
+++ b/t/t5411/test-0021-report-ng--porcelain.sh
@@ -25,12 +25,10 @@ test_expect_success "proc-receive: fail to update (ng, no message, $PROTOCOL/por
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (ng message, $PROTOCOL/porcelain)" '
@@ -60,10 +58,8 @@ test_expect_success "proc-receive: fail to update (ng, with message, $PROTOCOL/p
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0022-report-unexpect-ref.sh b/t/t5411/test-0022-report-unexpect-ref.sh
index b0c5a6e168..dbed467186 100644
--- a/t/t5411/test-0022-report-unexpect-ref.sh
+++ b/t/t5411/test-0022-report-unexpect-ref.sh
@@ -30,12 +30,10 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/main/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)
diff --git a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
index 212927b8dc..e89096fa13 100644
--- a/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
+++ b/t/t5411/test-0023-report-unexpect-ref--porcelain.sh
@@ -31,12 +31,10 @@ test_expect_success "proc-receive: report unexpected ref ($PROTOCOL/porcelain)"
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)
diff --git a/t/t5411/test-0024-report-unknown-ref.sh b/t/t5411/test-0024-report-unknown-ref.sh
index 94ffad55f2..77204244b8 100644
--- a/t/t5411/test-0024-report-unknown-ref.sh
+++ b/t/t5411/test-0024-report-unknown-ref.sh
@@ -25,10 +25,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/a/b/c/my/topic (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0025-report-unknown-ref--porcelain.sh b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
index 8ee7e66472..eeb1ce6b2c 100644
--- a/t/t5411/test-0025-report-unknown-ref--porcelain.sh
+++ b/t/t5411/test-0025-report-unknown-ref--porcelain.sh
@@ -26,10 +26,8 @@ test_expect_success "proc-receive: report unknown reference ($PROTOCOL/porcelain
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
index bb2cf59bdc..1ec2cb95bc 100644
--- a/t/t5411/test-0026-push-options.sh
+++ b/t/t5411/test-0026-push-options.sh
@@ -20,12 +20,10 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
 	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "enable push options ($PROTOCOL)" '
@@ -69,13 +67,11 @@ test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL)
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "restore proc-receive hook ($PROTOCOL)" '
@@ -123,13 +119,11 @@ test_expect_success "proc-receive: push with options ($PROTOCOL)" '
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
diff --git a/t/t5411/test-0027-push-options--porcelain.sh b/t/t5411/test-0027-push-options--porcelain.sh
index 54e4f8f1d2..447fbfec0c 100644
--- a/t/t5411/test-0027-push-options--porcelain.sh
+++ b/t/t5411/test-0027-push-options--porcelain.sh
@@ -21,12 +21,10 @@ test_expect_success "proc-receive: not support push options ($PROTOCOL/porcelain
 	make_user_friendly_and_stable_output <out-$test_count >actual &&
 	test_i18ngrep "fatal: the receiving end does not support push options" \
 		actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "enable push options ($PROTOCOL/porcelain)" '
@@ -72,13 +70,11 @@ test_expect_success "proc-receive: ignore push-options for version 0 ($PROTOCOL/
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "restore proc-receive hook ($PROTOCOL/porcelain)" '
@@ -128,13 +124,11 @@ test_expect_success "proc-receive: push with options ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/next
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             next(A)
diff --git a/t/t5411/test-0030-report-ok.sh b/t/t5411/test-0030-report-ok.sh
index 5d6feef118..8acb4f204f 100644
--- a/t/t5411/test-0030-report-ok.sh
+++ b/t/t5411/test-0030-report-ok.sh
@@ -26,10 +26,8 @@ test_expect_success "proc-receive: ok ($PROTOCOL)" '
 	 * [new reference] HEAD -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0031-report-ok--porcelain.sh b/t/t5411/test-0031-report-ok--porcelain.sh
index 91666d32df..a967718046 100644
--- a/t/t5411/test-0031-report-ok--porcelain.sh
+++ b/t/t5411/test-0031-report-ok--porcelain.sh
@@ -27,10 +27,8 @@ test_expect_success "proc-receive: ok ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0032-report-with-options.sh b/t/t5411/test-0032-report-with-options.sh
index e6aa0f7018..437ade012d 100644
--- a/t/t5411/test-0032-report-with-options.sh
+++ b/t/t5411/test-0032-report-with-options.sh
@@ -247,10 +247,7 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL)" '
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0033-report-with-options--porcelain.sh b/t/t5411/test-0033-report-with-options--porcelain.sh
index a8ca5dd0dd..11486720ee 100644
--- a/t/t5411/test-0033-report-with-options--porcelain.sh
+++ b/t/t5411/test-0033-report-with-options--porcelain.sh
@@ -256,10 +256,7 @@ test_expect_success "proc-receive: report with multiple rewrites ($PROTOCOL/porc
 	EOF
 	test_cmp expect actual &&
 
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0034-report-ft.sh b/t/t5411/test-0034-report-ft.sh
index c355c290d2..6e0d08b327 100644
--- a/t/t5411/test-0034-report-ft.sh
+++ b/t/t5411/test-0034-report-ft.sh
@@ -28,13 +28,11 @@ test_expect_success "proc-receive: fall throught, let receive-pack to execute ($
 	 * [new reference] <COMMIT-B> -> refs/for/main/topic
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/for/main/topic
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             refs/for/main/topic(A)
diff --git a/t/t5411/test-0035-report-ft--porcelain.sh b/t/t5411/test-0035-report-ft--porcelain.sh
index 8ce4e58f2a..81bae9f2ec 100644
--- a/t/t5411/test-0035-report-ft--porcelain.sh
+++ b/t/t5411/test-0035-report-ft--porcelain.sh
@@ -29,13 +29,11 @@ test_expect_success "proc-receive: fall throught, let receive-pack to execute ($
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/for/main/topic
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             refs/for/main/topic(A)
diff --git a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
index fad8eea8a0..be9b18b2b6 100644
--- a/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
+++ b/t/t5411/test-0036-report-multi-rewrite-for-one-ref.sh
@@ -65,12 +65,10 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	 <OID-A>..<OID-B> HEAD -> refs/changes/25/125/1
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #1 ($PROTOCOL)" '
@@ -142,12 +140,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	 + <OID-B>...<OID-A> HEAD -> refs/changes/25/125/1 (forced update)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #2 ($PROTOCOL)" '
@@ -205,12 +201,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL)" '
 	 <OID-A>..<OID-B> HEAD -> refs/changes/24/124/2
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "proc-receive: check remote-tracking #3 ($PROTOCOL)" '
diff --git a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
index dc254d57eb..95fb89c031 100644
--- a/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
+++ b/t/t5411/test-0037-report-multi-rewrite-for-one-ref--porcelain.sh
@@ -51,12 +51,10 @@ test_expect_success "proc-receive: multiple rewrite for one ref, no refname for
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, no refname for the 2nd rewrite, $PROTOCOL/porcelain)" '
@@ -114,12 +112,10 @@ test_expect_success "proc-receive: multiple rewrites for one ref, no refname for
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook (multiple rewrites for one ref, $PROTOCOL/porcelain)" '
@@ -163,10 +159,8 @@ test_expect_success "proc-receive: multiple rewrites for one ref ($PROTOCOL/porc
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
diff --git a/t/t5411/test-0038-report-mixed-refs.sh b/t/t5411/test-0038-report-mixed-refs.sh
index 286f80d423..5e005299cc 100644
--- a/t/t5411/test-0038-report-mixed-refs.sh
+++ b/t/t5411/test-0038-report-mixed-refs.sh
@@ -65,15 +65,13 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL)" '
 	 ! [remote rejected] HEAD -> refs/for/next/topic3 (proc-receive failed to report status)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)             foo(A)  bar(A))  baz(A)
diff --git a/t/t5411/test-0039-report-mixed-refs--porcelain.sh b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
index 939b8a3b4f..8f891c5385 100644
--- a/t/t5411/test-0039-report-mixed-refs--porcelain.sh
+++ b/t/t5411/test-0039-report-mixed-refs--porcelain.sh
@@ -66,15 +66,13 @@ test_expect_success "proc-receive: report update of mixed refs ($PROTOCOL/porcel
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/foo
 	<COMMIT-B> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(B)             foo(A)  bar(A))  baz(A)
diff --git a/t/t5411/test-0040-process-all-refs.sh b/t/t5411/test-0040-process-all-refs.sh
index 2565302a17..fdcdcc7c2e 100644
--- a/t/t5411/test-0040-process-all-refs.sh
+++ b/t/t5411/test-0040-process-all-refs.sh
@@ -92,14 +92,12 @@ test_expect_success "proc-receive: process all refs ($PROTOCOL)" '
 	 + <OID-B>...<OID-A> HEAD -> refs/pull/124/head (forced update)
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             bar(A)  baz(B)
diff --git a/t/t5411/test-0041-process-all-refs--porcelain.sh b/t/t5411/test-0041-process-all-refs--porcelain.sh
index e21420b60d..73b35fe0aa 100644
--- a/t/t5411/test-0041-process-all-refs--porcelain.sh
+++ b/t/t5411/test-0041-process-all-refs--porcelain.sh
@@ -93,14 +93,12 @@ test_expect_success "proc-receive: process all refs ($PROTOCOL/porcelain)" '
 	Done
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-B> refs/heads/bar
 	<COMMIT-A> refs/heads/baz
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)             bar(A)  baz(B)
diff --git a/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
index 2e29518ec5..7214647ada 100644
--- a/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
+++ b/t/t5411/test-0050-proc-receive-refs-with-modifiers.sh
@@ -50,12 +50,10 @@ test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
 	 * [new reference] v123 -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	EOF
-	test_cmp expect actual
 '
 
 # Refs of upstream : main(A)
@@ -63,14 +61,12 @@ test_expect_success "proc-receive: update branch and new tag ($PROTOCOL)" '
 test_expect_success "setup upstream: create tags/v123 ($PROTOCOL)" '
 	git -C "$upstream" update-ref refs/heads/topic $A &&
 	git -C "$upstream" update-ref refs/tags/v123 $TAG &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-A> refs/heads/topic
 	<TAG-v123> refs/tags/v123
 	EOF
-	test_cmp expect actual
 '
 
 test_expect_success "setup proc-receive hook ($PROTOCOL)" '
@@ -125,11 +121,9 @@ test_expect_success "proc-receive: create/delete branch, and delete tag ($PROTOC
 	 * [new reference] <COMMIT-A> -> refs/pull/124/head
 	EOF
 	test_cmp expect actual &&
-	git -C "$upstream" show-ref >out &&
-	make_user_friendly_and_stable_output <out >actual &&
-	cat >expect <<-EOF &&
+
+	test_cmp_refs -C "$upstream" <<-EOF
 	<COMMIT-A> refs/heads/main
 	<COMMIT-B> refs/heads/topic
 	EOF
-	test_cmp expect actual
 '
-- 
2.28.0.15.gba9e81f0bd


^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v19 03/10] receive-pack: add new proc-receive hook
  2021-01-21  2:21                                                     ` Jiang Xin
@ 2021-01-21  6:12                                                       ` SZEDER Gábor
  0 siblings, 0 replies; 266+ messages in thread
From: SZEDER Gábor @ 2021-01-21  6:12 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Junio C Hamano, Git List, Jiang Xin

On Thu, Jan 21, 2021 at 10:21:36AM +0800, Jiang Xin wrote:
> SZEDER Gábor <szeder.dev@gmail.com> 于2021年1月20日周三 下午8:29写道:
> >
> > On Mon, Jan 18, 2021 at 04:24:11PM +0800, Jiang Xin wrote:
> > > SZEDER Gábor <szeder.dev@gmail.com> 于2021年1月18日周一 上午6:21写道:
> > > >
> > > >
> > > > This patch added a whole lot of test cases, and two of them '86 -
> > > > proc-receive: not support push options (builtin protocol)' and '95 -
> > > > proc-receive: not support push options (builtin protocol/porcelain)'
> > > > are prone to rare failures.
> > > >
> > > > On Thu, Aug 27, 2020 at 11:45:44AM -0400, Jiang Xin wrote:
> > > > > diff --git a/t/t5411/test-0026-push-options.sh b/t/t5411/test-0026-push-options.sh
> > > > > new file mode 100644
> > > > > index 0000000000..d0c4da8b23
> > > > > --- /dev/null
> > > > > +++ b/t/t5411/test-0026-push-options.sh
> > > >
> > > > > +# Refs of upstream : master(A)
> > > > > +# Refs of workbench: master(A)  tags/v123
> > > > > +# git push -o ...  :                       refs/for/master/topic
> > > > > +test_expect_success "proc-receive: not support push options ($PROTOCOL)" '
> > > > > +     test_must_fail git -C workbench push \
> > > > > +             -o issue=123 \
> > > > > +             -o reviewer=user1 \
> > > > > +             origin \
> > > > > +             HEAD:refs/for/master/topic \
> > > > > +             >out 2>&1 &&
> > > >
> > > > Three relevant things are happening here:
> > > >
> > > >   - 'git push' is executed with its standard output and error
> > > >     redirected to the file 'out'.
> > > >
> > > >   - 'git push' executes 'git receive-pack' internally, which inherits
> > > >     the open file descriptors, so its output and error goes into that
> > > >     same 'out' file.
> > > >
> > > >   - 'git push' is expected to fail when it finds out that the other
> > > >     side doesn't support push options, but it does so with a simple
> > > >     die() right away, without waiting for its child 'git receive-pack'
> > > >     process to finish.
> > > >
> > > > > +     make_user_friendly_and_stable_output <out >actual &&
> > > > > +     test_i18ngrep "fatal: the receiving end does not support push options" \
> > > > > +             actual &&
> > > > > +     git -C "$upstream" show-ref >out &&
> > > >
> > > > Here the shell opens and truncates the file 'out' to write 'git
> > > > show-ref's output, i.e. it is still the same 'out' file that was used
> > > > earlier.
> > > >
> > > > Consequently, it is possible that 'git receive-pack' is still running,
> > > > its open file descriptors to 'out' are still valid, and its "fatal:
> > > > the remote end hung up unexpectedly" error message about the suddenly
> > > > disappeared 'git push' can partially overwrite the output from 'git
> > > > show-ref'.
> >
> >
> > > > I think these are the only two tests that can cause this racy
> > > > behavior: by instrumenting finish_command() I found that in all other
> > > > tests where 'git push' is expected to fail it errors out gracefully
> > > > and waits for its 'git receive-pack' child process.
> > >
> > > Atomic push may have the same problem.
> >
> > I don't think so, because send_pack() doesn't die() when a ref is
> > rejected in an atomic push, but returns, and lets its caller terminate
> > in an usual way, including waiting for 'git receive-pack'.
> 
> I find many places where the client side will die() before closing the
> service side gracefully:
> 
>  + In `transport_push()`, if fail to push a submodule, will die().
>  + In `git_transport_push()`, will die() for an unimplemented v2 protocol.
>  + In `send_pack()`, will die() if
>     the server side has an incompatible hash algorithm, or
>     the receiving end does not support --signed push, or
>     the receiving end does not support --atomic push, or
>     the receiving end does not support push options

Sure, but it seems that those are not covered in thet 5411.

You can build Git with the patch below, run t5411 with '-V -x', and
then look through all the cases to see where 'git push' waited or did
not wait for its 'git receive-pack' child process.

diff --git a/run-command.c b/run-command.c
index ea4d0fb4b1..4accdb343e 100644
--- a/run-command.c
+++ b/run-command.c
@@ -988,6 +988,7 @@ int start_command(struct child_process *cmd)
 int finish_command(struct child_process *cmd)
 {
 	int ret = wait_or_whine(cmd->pid, cmd->argv[0], 0);
+	dprintf(3, "finish_command(): '%s'\n", cmd->argv[0]);
 	trace2_child_exit(cmd, ret);
 	child_process_clear(cmd);
 	return ret;

^ permalink raw reply related	[flat|nested] 266+ messages in thread

* Re: [PATCH v3 0/2] use unique out file in t5411
  2021-01-21  2:53                                                         ` [PATCH v3 0/2] use unique out file in t5411 Jiang Xin
@ 2021-02-11 21:52                                                           ` Junio C Hamano
  2021-02-13 15:13                                                             ` Jiang Xin
  0 siblings, 1 reply; 266+ messages in thread
From: Junio C Hamano @ 2021-02-11 21:52 UTC (permalink / raw)
  To: Jiang Xin; +Cc: Git List, SZEDER Gábor, Johannes Sixt, Jiang Xin

Jiang Xin <worldhello.net@gmail.com> writes:

> From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
>
> t5411 has some test cases for the behavior of failed 'git push'
> commands.  Even the 'git push' command ended, the output file
> may still be updated with messages from 'git-receive-pack' command
> running in background.  This breaks test cases which reuse the
> same 'out' file.

I do not recall hearing from SZEDER who raised concerns about "a lot
of churn that can be avoided by a change in the code to die more
gracefully" in his previous round's review.  This has been in 'seen'
for a long time, so let's move it down to 'next'.

Thanks.

^ permalink raw reply	[flat|nested] 266+ messages in thread

* Re: [PATCH v3 0/2] use unique out file in t5411
  2021-02-11 21:52                                                           ` Junio C Hamano
@ 2021-02-13 15:13                                                             ` Jiang Xin
  0 siblings, 0 replies; 266+ messages in thread
From: Jiang Xin @ 2021-02-13 15:13 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Git List, SZEDER Gábor, Johannes Sixt, Jiang Xin

Junio C Hamano <gitster@pobox.com> 于2021年2月12日周五 上午5:52写道:
>
> Jiang Xin <worldhello.net@gmail.com> writes:
>
> > From: Jiang Xin <zhiyou.jx@alibaba-inc.com>
> >
> > t5411 has some test cases for the behavior of failed 'git push'
> > commands.  Even the 'git push' command ended, the output file
> > may still be updated with messages from 'git-receive-pack' command
> > running in background.  This breaks test cases which reuse the
> > same 'out' file.
>
> I do not recall hearing from SZEDER who raised concerns about "a lot
> of churn that can be avoided by a change in the code to die more
> gracefully" in his previous round's review.  This has been in 'seen'
> for a long time, so let's move it down to 'next'.
>

SZEDER's concerns about "a lot of churn that can be avoided ..." is in
this post:

    https://public-inbox.org/git/20210120124906.GA8396@szeder.dev/

I think current implementation of the client side of git-push is OK.
It's no necessary to add more code and more roundtrips for git-push
client side to complete the protocol in order to wait for end of the
server.

--
Jiang Xin

^ permalink raw reply	[flat|nested] 266+ messages in thread

end of thread, other threads:[~2021-02-13 15:14 UTC | newest]

Thread overview: 266+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-03-04 11:33 [PATCH 0/7] New execute-commands hook for centralized workflow Jiang Xin
2020-03-04 11:33 ` [PATCH 1/7] receive-pack: new external execute-commands hook Jiang Xin
2020-03-04 11:33 ` [PATCH 2/7] receive-pack: feed all commands to post-receive Jiang Xin
2020-03-04 11:33 ` [PATCH 3/7] receive-pack: try `execute-commands --pre-receive` Jiang Xin
2020-03-04 11:33 ` [PATCH 4/7] receive-pack: read env from execute-commands output Jiang Xin
2020-03-04 11:33 ` [PATCH 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-03-04 11:33 ` [PATCH 6/7] receive-pack: new config receive.executeCommandsHookRefs Jiang Xin
2020-03-04 11:33 ` [PATCH 7/7] hook: add document and example for "execute-commands" hook Jiang Xin
2020-03-04 20:39 ` [PATCH 0/7] New execute-commands hook for centralized workflow Junio C Hamano
2020-03-05 16:51   ` Jiang Xin
2020-03-08 14:56     ` [PATCH v2 0/5] New proc-receive " Jiang Xin
2020-03-08 14:56     ` [PATCH v2 1/5] receive-pack: add new proc-receive hook Jiang Xin
2020-03-09 17:12       ` Junio C Hamano
2020-03-10  6:03         ` Jiang Xin
2020-03-13 12:23           ` [PATCH v3 0/4] New proc-receive hook for centralized workflow Jiang Xin
2020-03-22 13:18             ` [PATCH v4 0/5] " Jiang Xin
2020-03-25  5:19               ` Junio C Hamano
2020-03-22 13:18             ` [PATCH v4 1/5] transport: not report a non-head push as a branch Jiang Xin
2020-03-25  6:04               ` Junio C Hamano
2020-03-22 13:18             ` [PATCH v4 2/5] receive-pack: add new proc-receive hook Jiang Xin
2020-03-25 14:36               ` [PATCH 0/3] Never report references we not push Jiang Xin
2020-03-29 14:33                 ` [PATCH v2 0/4] " Jiang Xin
2020-03-29 14:35                   ` Jiang Xin
2020-04-16 16:24                   ` [PATCH v3 0/5] fix git-push porcelain output and atomic report issue Jiang Xin
2020-04-17  9:45                     ` [PATCH v4 " Jiang Xin
2020-04-17  9:45                     ` [PATCH v4 1/5] send-pack: fix inconsistent porcelain output Jiang Xin
2020-04-17 19:51                       ` Junio C Hamano
2020-04-17  9:45                     ` [PATCH v4 2/5] t5543: never report what we do not push Jiang Xin
2020-04-17  9:45                     ` [PATCH v4 3/5] send-pack: mark failure of atomic push properly Jiang Xin
2020-04-17  9:45                     ` [PATCH v4 4/5] transport-helper: mark failure for atomic push Jiang Xin
2020-04-17  9:45                     ` [PATCH v4 5/5] transport-helper: new method reject_atomic_push() Jiang Xin
2020-04-16 16:24                   ` [PATCH v3 1/5] send-pack: fix inconsistent porcelain output Jiang Xin
2020-04-16 16:24                   ` [PATCH v3 2/5] t5543: never report what we do not push Jiang Xin
2020-04-16 16:24                   ` [PATCH v3 3/5] send-pack: mark failure of atomic push properly Jiang Xin
2020-04-16 16:24                   ` [PATCH v3 4/5] transport-helper: mark failure for atomic push Jiang Xin
2020-04-16 16:24                   ` [PATCH v3 5/5] transport-helper: new method reject_atomic_push() Jiang Xin
2020-03-29 14:33                 ` [PATCH v2 1/4] t5543: never report what we do not push Jiang Xin
2020-03-29 14:33                 ` [PATCH v2 2/4] send-pack: mark failure of atomic push properly Jiang Xin
2020-03-29 14:33                 ` [PATCH v2 3/4] transport-helper: mark failure for atomic push Jiang Xin
2020-03-29 14:33                 ` [PATCH v2 4/4] transport-helper: new method reject_atomic_push() Jiang Xin
2020-03-25 14:36               ` [PATCH 1/3] t5543: never report what we do not push Jiang Xin
2020-03-25 15:05                 ` Junio C Hamano
2020-03-26  2:25                   ` Jiang Xin
2020-03-25 14:36               ` [PATCH 2/3] send-pack: mark failure of atomic push properly Jiang Xin
2020-03-25 15:15                 ` Junio C Hamano
2020-03-25 14:36               ` [PATCH 3/3] transport-helper: enforce atomic in push_refs_with_push Jiang Xin
2020-03-25 15:32                 ` Junio C Hamano
2020-03-22 13:18             ` [PATCH v4 3/5] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-03-22 13:18             ` [PATCH v4 4/5] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-03-22 13:18             ` [PATCH v4 5/5] receive-pack: refactor report for proc-receive Jiang Xin
2020-03-13 12:23           ` [PATCH v3 1/4] receive-pack: add new proc-receive hook Jiang Xin
2020-03-13 12:23           ` [PATCH v3 2/4] receive-pack: refactor report for proc-receive Jiang Xin
2020-03-13 12:23           ` [PATCH v3 3/4] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-03-13 12:23           ` [PATCH v3 4/4] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-03-08 14:56     ` [PATCH v2 2/5] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-03-08 15:38     ` [PATCH v2 3/5] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-03-08 15:38     ` [PATCH v2 4/5] receive-pack: read env from proc-receive output Jiang Xin
2020-03-08 15:38     ` [PATCH v2 5/5] hook: add document and example for "proc-receive" hook Jiang Xin
2020-03-30 16:57 ` [PATCH v5 0/6] New proc-receive hook for centralized workflow Jiang Xin
2020-03-30 16:57 ` [PATCH v5 1/6] transport: not report a non-head push as a branch Jiang Xin
2020-03-30 16:57 ` [PATCH v5 2/6] receive-pack: add new proc-receive hook Jiang Xin
2020-03-31  0:19   ` Junio C Hamano
2020-03-31  0:21   ` Junio C Hamano
2020-03-30 16:57 ` [PATCH v5 3/6] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-03-30 16:57 ` [PATCH v5 4/6] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-03-30 16:57 ` [PATCH v5 5/6] receive-pack: refactor report for proc-receive Jiang Xin
2020-03-30 16:57 ` [PATCH v5 6/6] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-02 16:35 ` [PATCH v6 0/7] New proc-receive hook for centralized workflow Jiang Xin
2020-04-02 18:26   ` Junio C Hamano
2020-04-03 16:08     ` [PATCH v7 " Jiang Xin
2020-04-04 13:43       ` [PATCH v8 " Jiang Xin
2020-04-07 12:08         ` [PATCH v9 0/6] " Jiang Xin
2020-04-12 13:30           ` [PATCH v10 0/8] " Jiang Xin
2020-04-13 16:48             ` [PATCH v11 0/7] " Jiang Xin
2020-04-13 16:48             ` [PATCH v11 1/7] transport: not report a non-head push as a branch Jiang Xin
2020-04-13 16:48             ` [PATCH v11 2/7] connect: export parse_feature_value() Jiang Xin
2020-04-13 16:48             ` [PATCH v11 3/7] receive-pack: add new proc-receive hook Jiang Xin
2020-04-13 16:48             ` [PATCH v11 4/7] send-pack: extension for client-side status report Jiang Xin
2020-04-13 16:48             ` [PATCH v11 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-13 16:48             ` [PATCH v11 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-13 16:48             ` [PATCH v11 7/7] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-12 13:30           ` [PATCH v10 1/8] transport: not report a non-head push as a branch Jiang Xin
2020-04-12 20:26             ` Junio C Hamano
2020-04-13 11:15               ` Jiang Xin
2020-04-12 13:30           ` [PATCH v10 2/8] receive-pack: add new proc-receive hook Jiang Xin
2020-04-12 21:30             ` Junio C Hamano
2020-04-13 10:58               ` Jiang Xin
2020-04-13 21:50                 ` Junio C Hamano
2020-04-14 12:32                   ` [PATCH v12 0/7] New proc-receive hook for centralized workflow Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 0/8] " Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 1/8] transport: not report a non-head push as a branch Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 2/8] connect: export parse_feature_value() Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 3/8] receive-pack: add new proc-receive hook Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 4/8] send-pack: extension for client-side status report Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 5/8] receive-pack: feed extended_status to post-receive Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 6/8] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 7/8] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-18 16:03                     ` [PATCH v13 8/8] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-14 12:32                   ` [PATCH v12 1/7] transport: not report a non-head push as a branch Jiang Xin
2020-04-14 12:32                   ` [PATCH v12 2/7] connect: export parse_feature_value() Jiang Xin
2020-04-14 12:32                   ` [PATCH v12 3/7] receive-pack: add new proc-receive hook Jiang Xin
2020-04-15 15:48                     ` Junio C Hamano
2020-04-15 15:55                       ` Jiang Xin
2020-04-15 18:34                         ` Junio C Hamano
2020-04-27 17:00                           ` Jiang Xin
2020-04-29  7:56                             ` Jeff King
2020-04-30 15:33                               ` Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 0/7] New proc-receive hook for centralized workflow Jiang Xin
2020-05-06 23:14                                   ` Junio C Hamano
2020-05-07  1:37                                     ` Jiang Xin
2020-05-07 11:18                                       ` Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 " Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 00/11] " Jiang Xin
2020-08-15 17:17                                         ` [PATCH v17 00/10] " Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 " Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 " Jiang Xin
2020-08-27 19:57                                               ` Junio C Hamano
2020-08-27 15:45                                             ` [PATCH v19 01/10] transport: not report a non-head push as a branch Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 03/10] receive-pack: add new " Jiang Xin
2020-11-04 22:15                                               ` Johannes Schindelin
2020-11-04 22:58                                                 ` Johannes Schindelin
2020-11-05 14:54                                                 ` Jiang Xin
2020-11-05 15:23                                                   ` [RFC PATCH] t5411: fix broken pipe write error on proc-receive Jiang Xin
2020-11-05 19:14                                                     ` Junio C Hamano
2020-11-07  2:57                                                       ` [PATCH] t5411: consistent result for proc-receive broken test Jiang Xin
2020-11-09  7:29                                                         ` Jiang Xin
2020-11-09 10:58                                                           ` [PATCH v2] " Jiang Xin
2020-11-09 20:59                                                             ` Junio C Hamano
2020-11-09 23:12                                                               ` Jeff King
2020-11-09 23:22                                                                 ` Junio C Hamano
2020-11-10  0:03                                                                   ` Jeff King
2020-11-10 11:49                                                                 ` Jiang Xin
2020-11-10 12:01                                                                   ` [PATCH v3 1/2] t5411: refactor make_user_friendly_and_stable_output Jiang Xin
2020-11-10 20:51                                                                     ` Junio C Hamano
2020-11-11 11:08                                                                       ` Jiang Xin
2020-11-10 12:01                                                                   ` [PATCH v3 2/2] receive-pack: gently write messages to proc-receive Jiang Xin
2020-11-10 21:52                                                                     ` Jeff King
2020-11-11 11:03                                                                       ` Jiang Xin
2020-11-10 21:00                                                                   ` [PATCH v2] t5411: consistent result for proc-receive broken test Junio C Hamano
2020-11-10 21:13                                                                     ` Junio C Hamano
2020-11-11 11:31                                                                   ` [PATCH v4 0/3] jx/t5411-flake-fix Jiang Xin
2020-11-11 11:32                                                                   ` [PATCH v4 1/3] t5411: new helper filter_out_user_friendly_and_stable_output Jiang Xin
2020-11-11 11:32                                                                   ` [PATCH v4 2/3] receive-pack: gently write messages to proc-receive Jiang Xin
2020-11-11 11:32                                                                   ` [PATCH v4 3/3] receive-pack: use default version 0 for proc-receive Jiang Xin
2020-11-10 11:44                                                               ` [PATCH v2] t5411: consistent result for proc-receive broken test Jiang Xin
2020-11-05 18:39                                                   ` [PATCH v19 03/10] receive-pack: add new proc-receive hook Junio C Hamano
2021-01-17 22:21                                               ` SZEDER Gábor
2021-01-18  8:24                                                 ` Jiang Xin
2021-01-20 12:28                                                   ` SZEDER Gábor
2021-01-21  2:21                                                     ` Jiang Xin
2021-01-21  6:12                                                       ` SZEDER Gábor
2021-01-18 13:30                                                 ` [PATCH 1/2] t5411: remove file after use to prevent overwriting Jiang Xin
2021-01-18 18:21                                                   ` Johannes Sixt
2021-01-19  0:48                                                     ` Jiang Xin
2021-01-19 10:24                                                     ` [PATCH v2 0/2] t5411 out file overwrite fix Jiang Xin
2021-01-19 10:24                                                     ` [PATCH v2 1/2] t5411: use different out file to prevent overwriting Jiang Xin
2021-01-20 12:49                                                       ` SZEDER Gábor
2021-01-21  1:59                                                         ` Jiang Xin
2021-01-21  2:53                                                         ` [PATCH v3 0/2] use unique out file in t5411 Jiang Xin
2021-02-11 21:52                                                           ` Junio C Hamano
2021-02-13 15:13                                                             ` Jiang Xin
2021-01-21  2:53                                                         ` [PATCH v3 1/2] t5411: use different out file to prevent overwriting Jiang Xin
2021-01-21  2:53                                                         ` [PATCH v3 2/2] t5411: refactor check of refs using test_cmp_refs Jiang Xin
2021-01-19 10:24                                                     ` [PATCH v2 " Jiang Xin
2021-01-18 13:30                                                 ` [PATCH " Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 04/10] receive-pack: feed report options to post-receive Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 05/10] New capability "report-status-v2" for git-push Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 06/10] doc: add document for capability report-status-v2 Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 08/10] t5411: test updates of remote-tracking branches Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 09/10] transport: parse report options for tracking refs Jiang Xin
2020-08-27 15:45                                             ` [PATCH v19 10/10] doc: add documentation for the proc-receive hook Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 01/10] transport: not report a non-head push as a branch Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 03/10] receive-pack: add new " Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 04/10] receive-pack: feed report options to post-receive Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 05/10] New capability "report-status-v2" for git-push Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 06/10] doc: add document for capability report-status-v2 Jiang Xin
2020-08-24 17:41                                           ` [PATCH v18 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-08-24 17:42                                           ` [PATCH v18 08/10] t5411: test updates of remote-tracking branches Jiang Xin
2020-08-24 17:42                                           ` [PATCH v18 09/10] transport: parse report options for tracking refs Jiang Xin
2020-08-24 17:42                                           ` [PATCH v18 10/10] doc: add documentation for the proc-receive hook Jiang Xin
2020-08-15 17:17                                         ` [PATCH v17 01/10] transport: not report a non-head push as a branch Jiang Xin
2020-08-15 17:17                                         ` [PATCH v17 02/10] t5411: add basic test cases for proc-receive hook Jiang Xin
2020-08-15 17:17                                         ` [PATCH v17 03/10] receive-pack: add new " Jiang Xin
2020-08-17 20:53                                           ` Junio C Hamano
2020-08-15 17:17                                         ` [PATCH v17 04/10] New capability "report-status-v2" for git-push Jiang Xin
2020-08-17 21:12                                           ` Junio C Hamano
2020-08-15 17:17                                         ` [PATCH v17 05/10] doc: add document for capability report-status-v2 Jiang Xin
2020-08-15 17:17                                         ` [PATCH v17 06/10] receive-pack: feed report options to post-receive Jiang Xin
2020-08-17 21:15                                           ` Junio C Hamano
2020-08-15 17:17                                         ` [PATCH v17 07/10] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-08-17 21:25                                           ` Junio C Hamano
2020-08-15 17:17                                         ` [PATCH v17 08/10] t5411: test updates of remote-tracking branches Jiang Xin
2020-08-15 17:17                                         ` [PATCH v17 09/10] transport: parse report options for tracking refs Jiang Xin
2020-08-15 17:17                                         ` [PATCH v17 10/10] doc: add documentation for the proc-receive hook Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 01/11] transport: not report a non-head push as a branch Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 02/11] t5411: add basic test cases for proc-receive hook Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 03/11] receive-pack: add new " Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 04/11] New capability "report-status-v2" for git-push Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 05/11] doc: add document for capability report-status-v2 Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 06/11] receive-pack: feed report options to post-receive Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 07/11] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 08/11] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 09/11] t5411: test updates of remote-tracking branches Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 10/11] transport: parse report options for tracking refs Jiang Xin
2020-05-18  9:40                                       ` [PATCH v16 11/11] doc: add documentation for the proc-receive hook Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 1/7] transport: not report a non-head push as a branch Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 2/7] receive-pack: add new proc-receive hook Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 3/7] New capability "report-status-v2" for git-push Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 4/7] receive-pack: feed report options to post-receive Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-05-07 16:10                                     ` [PATCH v15 7/7] doc: add documentation for the proc-receive hook Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 1/7] transport: not report a non-head push as a branch Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 2/7] receive-pack: add new proc-receive hook Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 3/7] New capability "report-status-v2" for git-push Jiang Xin
2020-05-05 15:25                                   ` [PATCH v14 8/7] fixup! " Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 4/7] receive-pack: feed report options to post-receive Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-05-05 14:41                                 ` [PATCH v14 7/7] doc: add documentation for the proc-receive hook Jiang Xin
2020-05-07 17:27                                 ` [PATCH v12 3/7] receive-pack: add new " Jeff King
2020-04-14 12:32                   ` [PATCH v12 4/7] send-pack: extension for client-side status report Jiang Xin
2020-04-15 20:36                     ` Junio C Hamano
2020-04-14 12:32                   ` [PATCH v12 5/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-14 12:32                   ` [PATCH v12 6/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-14 12:32                   ` [PATCH v12 7/7] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-12 13:30           ` [PATCH v10 3/8] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-12 21:38             ` Junio C Hamano
2020-04-13 11:16               ` Jiang Xin
2020-04-12 13:30           ` [PATCH v10 4/8] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-12 21:46             ` Junio C Hamano
2020-04-13 11:16               ` Jiang Xin
2020-04-12 13:30           ` [PATCH v10 5/8] connect: export parse_feature_value() Jiang Xin
2020-04-12 13:30           ` [PATCH v10 6/8] receive-pack: extension for server-side report Jiang Xin
2020-04-12 13:30           ` [PATCH v10 7/8] send-pack: extension for client-side status report Jiang Xin
2020-04-12 13:30           ` [PATCH v10 8/8] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-07 12:08         ` [PATCH v9 1/6] transport: not report a non-head push as a branch Jiang Xin
2020-04-07 12:08         ` [PATCH v9 2/6] receive-pack: add new proc-receive hook Jiang Xin
2020-04-07 12:08         ` [PATCH v9 3/6] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-07 12:08         ` [PATCH v9 4/6] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-07 12:08         ` [PATCH v9 5/6] receive-pack: refactor report for proc-receive Jiang Xin
2020-04-07 12:08         ` [PATCH v9 6/6] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-04 13:43       ` [PATCH v8 1/7] transport: not report a non-head push as a branch Jiang Xin
2020-04-04 13:43       ` [PATCH v8 2/7] receive-pack: add new proc-receive hook Jiang Xin
2020-04-04 13:43       ` [PATCH v8 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-04 13:43       ` [PATCH v8 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-04 13:43       ` [PATCH v8 5/7] receive-pack: refactor report for proc-receive Jiang Xin
2020-04-04 13:43       ` [PATCH v8 6/7] t5412: test the proc-receive hook on HTTP protocol Jiang Xin
2020-04-04 13:43       ` [PATCH v8 7/7] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-03 16:08     ` [PATCH v7 1/7] transport: not report a non-head push as a branch Jiang Xin
2020-04-03 16:08     ` [PATCH v7 2/7] receive-pack: add new proc-receive hook Jiang Xin
2020-04-03 16:08     ` [PATCH v7 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-03 16:08     ` [PATCH v7 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-03 16:08     ` [PATCH v7 5/7] receive-pack: refactor report for proc-receive Jiang Xin
2020-04-03 16:08     ` [PATCH v7 6/7] t5412: test proc-receive hook on HTTP protocol Jiang Xin
2020-04-03 16:08     ` [PATCH v7 7/7] doc: add documentation for the proc-receive hook Jiang Xin
2020-04-02 16:35 ` [PATCH v6 1/7] transport: not report a non-head push as a branch Jiang Xin
2020-04-02 16:35 ` [PATCH v6 2/7] receive-pack: add new proc-receive hook Jiang Xin
2020-04-02 16:35 ` [PATCH v6 3/7] refs.c: refactor to reuse ref_is_hidden() Jiang Xin
2020-04-02 16:35 ` [PATCH v6 4/7] receive-pack: new config receive.procReceiveRefs Jiang Xin
2020-04-02 16:35 ` [PATCH v6 5/7] receive-pack: refactor report for proc-receive Jiang Xin
2020-04-02 16:35 ` [PATCH v6 6/7] t5412: test proc-receive hook on HTTP protocol Jiang Xin
2020-04-02 16:35 ` [PATCH v6 7/7] doc: add documentation for the proc-receive hook Jiang Xin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).