@@ -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
~~~~~~~~~~~~~~
@@ -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
new file mode 100755
@@ -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
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