Git Mailing List Archive on lore.kernel.org
 help / color / 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
                   ` (14 more replies)
  0 siblings, 15 replies; 54+ 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] 54+ 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
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (12 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (11 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (9 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (6 subsequent siblings)
  14 siblings, 1 reply; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ 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
  2020-03-29 14:33                 ` [PATCH v2 1/4] t5543: never report what we do " Jiang Xin
                                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 54+ 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] 54+ 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; 54+ 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	[flat|nested] 54+ 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 " 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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; 54+ 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	[flat|nested] 54+ 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
  0 siblings, 0 replies; 54+ 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] 54+ 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
                   ` (5 subsequent siblings)
  14 siblings, 0 replies; 54+ 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] 54+ 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
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (3 subsequent siblings)
  14 siblings, 2 replies; 54+ 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	[flat|nested] 54+ 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
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
  2020-03-30 16:57 ` [PATCH v5 6/6] doc: add documentation for the proc-receive hook Jiang Xin
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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
  14 siblings, 0 replies; 54+ 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	[flat|nested] 54+ 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; 54+ 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] 54+ 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; 54+ 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] 54+ messages in thread

end of thread, back to index

Thread overview: 54+ 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-03-29 14:33                 ` [PATCH v2 1/4] t5543: never report what we do " 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

Git Mailing List Archive on lore.kernel.org

Archives are clonable:
	git clone --mirror https://lore.kernel.org/git/0 git/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 git git/ https://lore.kernel.org/git \
		git@vger.kernel.org
	public-inbox-index git

Example config snippet for mirrors

Newsgroup available over NNTP:
	nntp://nntp.lore.kernel.org/org.kernel.vger.git


AGPL code for this site: git clone https://public-inbox.org/public-inbox.git