threads / patch / 32473

patch, 4 partshooks: Add function to check if a hook exists

Subject: [PATCH 1/4] hooks: Add function to check if a hook exists

## tl;dr

22 messages between Dec 28, 2012 and Jan 15, 2013. Diffs are folded; open one to read it.

replies: 21people: 2as markdown or json

Aaron Schrab· Dec 28, 2012, 22:57 UTC · lore

[PATCH 0/4] pre-push hook support

There have been at least a couple of submissions to add support for a pre-push hook, which were rejected at least partially because they didn't provide enough information to a hook script for it to determine what was to be pushed any better than a separate wrapper around the 'git push' command would be able to do. In this series I attempt to address that problem.

The first two patches in this series do a little bit of refactoring in order to make it easier to call hooks with a variable number of arguments.

The third patch actually adds support for calling a pre-push hook. If it exists, it will be called with the name and URL of the destination remote (if a named remote isn't being used, the URL will be supplied for both) followed by another argument for each ref being pushed; these arguments take the form:

  <local ref>:<local sha1>:<remote ref>:<remote sha1>

This should provide enough information for a script to easily determine the set of commits that is being pushed, and thus make a decision if that should be allowed.

The final patch adds a sample pre-push hook script which will deny attempts to push commits that are marked as a work in progress.

Aaron Schrab (4):
  hooks: Add function to check if a hook exists
  hooks: support variable number of parameters
  push: Add support for pre-push hooks
  Add sample pre-push hook script
 Documentation/githooks.txt       |   28 ++++++++
 builtin/push.c                   |    1 +
 run-command.c                    |   35 ++++++---
 run-command.h                    |    3 +
 t/t5571-pre-push-hook.sh         |  145 ++++++++++++++++++++++++++++++++++++++
 templates/hooks--pre-push.sample |   63 +++++++++++++++++
 transport.c                      |   25 +++++++
 transport.h                      |    1 +
 8 files changed, 292 insertions(+), 9 deletions(-)
 create mode 100755 t/t5571-pre-push-hook.sh
 create mode 100644 templates/hooks--pre-push.sample
-- 
1.7.10.4
Aaron Schrab· Dec 28, 2012, 22:57 UTC · re: Aaron Schrab · lore

Create find_hook() function to determine if a given hook exists and is executable. If it is the path to the script will be returned, otherwise NULL is returned.

This is in support for an upcoming run_hook_argv() function which will expect the full path to the hook script as the first element in the argv_array. This also makes it simple for places that can use a hook to check if a hook exists before doing, possibly lengthy, setup work which would be pointless if no such hook is present.

The returned value is left as a static value from get_pathname() rather than a duplicate because it is anticipated that the return value will either be used as a boolean, or immediately added to an argv_array list which would result in it being duplicated at that point.

Signed-off-by: Aaron Schrab <aaron@schrab.com>
---
 run-command.c |   15 +++++++++++++--
 run-command.h |    1 +
 2 files changed, 14 insertions(+), 2 deletions(-)
Show changes to 2 files +14 −2

run-command.c, run-command.h

diff --git a/run-command.c b/run-command.c
index 3b982e4..49c8fa0 100644
--- a/run-command.c
+++ b/run-command.c
@@ -735,6 +735,15 @@ int finish_async(struct async *async)
 #endif
 }
 
+char *find_hook(const char *name)
+{
+	char *path = git_path("hooks/%s", name);
+	if (access(path, X_OK) < 0)
+		path = NULL;
+
+	return path;
+}
+
 int run_hook(const char *index_file, const char *name, ...)
 {
 	struct child_process hook;
@@ -744,11 +753,13 @@ int run_hook(const char *index_file, const char *name, ...)
 	va_list args;
 	int ret;
 
-	if (access(git_path("hooks/%s", name), X_OK) < 0)
+	p = find_hook(name);
+	if (!p)
 		return 0;
 
+	argv_array_push(&argv, p);
+
 	va_start(args, name);
-	argv_array_push(&argv, git_path("hooks/%s", name));
 	while ((p = va_arg(args, const char *)))
 		argv_array_push(&argv, p);
 	va_end(args);
diff --git a/run-command.h b/run-command.h
index 850c638..221ce33 100644
--- a/run-command.h
+++ b/run-command.h
@@ -45,6 +45,7 @@ int start_command(struct child_process *);
 int finish_command(struct child_process *);
 int run_command(struct child_process *);
 
+extern char *find_hook(const char *name);
 extern int run_hook(const char *index_file, const char *name, ...);
 
 #define RUN_COMMAND_NO_STDIN 1
-- 
1.7.10.4
Junio C Hamano· Dec 29, 2012, 02:08 UTC · re: Aaron Schrab · lore

Re: [PATCH 1/4] hooks: Add function to check if a hook exists

Aaron Schrab <aaron@schrab.com> writes:
> Create find_hook() function to determine if a given hook exists and is
> executable.  If it is the path to the script will be returned, otherwise
> NULL is returned.

Sounds like a sensible thing to do. To make sure the API is also sensible, all the existing hooks should be updated to use this API, no?

> This is in support for an upcoming run_hook_argv() function which will
> expect the full path to the hook script as the first element in the
> argv_array.  

There is currently a public function called run_hook() that squats on the good name with a kludgy API that is too specific to using separate index file. Back when it was a private helper in the implementation of "git commit", it was perfectly fine, but it was exported without giving much thought on the API.

If you are introducing a new run_hook_* function, give it a generic enough API that lets all the existing hook callers to use it. I would imagine that the API requirement may be modelled after run_command() API so that we can pass argv[] and tweak the hook's environ[], as well as feeding its stdin and possibly reading from its stdout. That would be very useful.

Aaron Schrab· Dec 29, 2012, 14:50 UTC · re: Junio C Hamano · lore

Re: [PATCH 1/4] hooks: Add function to check if a hook exists

At 18:08 -0800 28 Dec 2012, Junio C Hamano <gitster@pobox.com> wrote:
Show 9 quoted lines
>Aaron Schrab <aaron@schrab.com> writes:
>
>> Create find_hook() function to determine if a given hook exists and is
>> executable.  If it is the path to the script will be returned, otherwise
>> NULL is returned.
>
>Sounds like a sensible thing to do.  To make sure the API is also
>sensible, all the existing hooks should be updated to use this API,
>no?

I'd been trying to keep the changes limited. I'll see about modifying the existing places that run hooks in v2 of the series.

Show 16 quoted lines
>> This is in support for an upcoming run_hook_argv() function which will
>> expect the full path to the hook script as the first element in the
>> argv_array.
>
>There is currently a public function called run_hook() that squats
>on the good name with a kludgy API that is too specific to using
>separate index file.  Back when it was a private helper in the
>implementation of "git commit", it was perfectly fine, but it was
>exported without giving much thought on the API.
>
>If you are introducing a new run_hook_* function, give it a generic
>enough API that lets all the existing hook callers to use it.  I
>would imagine that the API requirement may be modelled after
>run_command() API so that we can pass argv[] and tweak the hook's
>environ[], as well as feeding its stdin and possibly reading from
>its stdout.  That would be very useful.

I think the attraction of the run_hook() API is its simplicity. It's currently a fairly thin wrapper around the run_command() API. I suspect that if the run_hook() API were made generic enough to support all of the existing hook callers it would greatly complicate the existing calls to run_hook() while not providing much benefit to hook callers which can't currently use it beyond what run_command() offers.

Since I'm going to be changing the interface for this hook in v2 of the series so that it will be more complicated than can be readily addressed with the run_hook() API (and will have use a fixed number of arguments anyway) I'll be dropping the run_hook_argv() function.

Junio C Hamano· Dec 29, 2012, 16:54 UTC · re: Aaron Schrab · lore

Re: [PATCH 1/4] hooks: Add function to check if a hook exists

Aaron Schrab <aaron@schrab.com> writes:
> Since I'm going to be changing the interface for this hook in v2 of
> the series so that it will be more complicated than can be readily
> addressed with the run_hook() API (and will have use a fixed number of
> arguments anyway) I'll be dropping the run_hook_argv() function.

Just to make sure there is no misunderstanding (sorry for sending the message without finishing it with this clarification at the end in the first place). I didn't mean that converting all of the existing callers must come earlier than introducing a new hook invoker.

I just wanted to make sure that we are aware that we are adding to our technical debt, if we are adding another that is also specialized; as the proposed interface looked sufficiently generic, it would be the ideal one to make _other_ ones thin wrappers around it to unify the various codepaths.

Thanks.
Aaron Schrab· Dec 28, 2012, 22:57 UTC · re: Aaron Schrab · lore

[PATCH 2/4] hooks: support variable number of parameters

Define the run_hook_argv() function to allow hooks to be created where the number of parameters to be passed is variable. The existing run_hook() function uses stdarg to allow it to receive a variable number of arguments, but the number of arguments that a given caller is passing is fixed at compile time. This function will allow the caller of a hook to determine the number of arguments to pass when preparing to call the hook.

The first use of this function will be for a pre-push hook which will add an argument for every reference which is to be pushed.

Signed-off-by: Aaron Schrab <aaron@schrab.com>
---
 run-command.c |   20 +++++++++++++-------
 run-command.h |    2 ++
 2 files changed, 15 insertions(+), 7 deletions(-)
Show changes to 2 files +15 −7

run-command.c, run-command.h

diff --git a/run-command.c b/run-command.c
index 49c8fa0..e07202b 100644
--- a/run-command.c
+++ b/run-command.c
@@ -2,7 +2,6 @@
 #include "run-command.h"
 #include "exec_cmd.h"
 #include "sigchain.h"
-#include "argv-array.h"
 
 #ifndef SHELL_PATH
 # define SHELL_PATH "/bin/sh"
@@ -746,10 +745,8 @@ char *find_hook(const char *name)
 
 int run_hook(const char *index_file, const char *name, ...)
 {
-	struct child_process hook;
 	struct argv_array argv = ARGV_ARRAY_INIT;
-	const char *p, *env[2];
-	char index[PATH_MAX];
+	const char *p;
 	va_list args;
 	int ret;
 
@@ -764,6 +761,17 @@ int run_hook(const char *index_file, const char *name, ...)
 		argv_array_push(&argv, p);
 	va_end(args);
 
+	ret = run_hook_argv(index_file, argv);
+	argv_array_clear(&argv);
+	return ret;
+}
+
+int run_hook_argv(const char *index_file, struct argv_array argv)
+{
+	struct child_process hook;
+	char index[PATH_MAX];
+	const char *env[2];
+
 	memset(&hook, 0, sizeof(hook));
 	hook.argv = argv.argv;
 	hook.no_stdin = 1;
@@ -775,7 +783,5 @@ int run_hook(const char *index_file, const char *name, ...)
 		hook.env = env;
 	}
 
-	ret = run_command(&hook);
-	argv_array_clear(&argv);
-	return ret;
+	return run_command(&hook);
 }
diff --git a/run-command.h b/run-command.h
index 221ce33..12faa5b 100644
--- a/run-command.h
+++ b/run-command.h
@@ -1,6 +1,7 @@
 #ifndef RUN_COMMAND_H
 #define RUN_COMMAND_H
 
+#include "argv-array.h"
 #ifndef NO_PTHREADS
 #include <pthread.h>
 #endif
@@ -47,6 +48,7 @@ int run_command(struct child_process *);
 
 extern char *find_hook(const char *name);
 extern int run_hook(const char *index_file, const char *name, ...);
+extern int run_hook_argv(const char *index_file, struct argv_array);
 
 #define RUN_COMMAND_NO_STDIN 1
 #define RUN_GIT_CMD	     2	/*If this is to be git sub-command */
-- 
1.7.10.4
Aaron Schrab· Dec 28, 2012, 22:57 UTC · re: Aaron Schrab · lore

[PATCH 3/4] push: Add support for pre-push hooks

Add support for a pre-push hook which can be used to determine if the set of refs to be pushed is suitable for the target repository. The hook should be supplied with:

 1. name of the remote being used, or the URL if not using a named
    remote
 2. the URL to which we're pushing
 3. descriptions of what references are to be pushed

Each reference to be pushed should be described in a separate parameter to the hook script in the form:

  <local ref>:<local sha1>:<remote ref>:<remote sha1>

This will allow the script to determine if the push is acceptable based on the target repository and branch(es), the commits which are to be pushed, and even the source branches in some cases.

Signed-off-by: Aaron Schrab <aaron@schrab.com>
---
 Documentation/githooks.txt |   28 +++++++++
 builtin/push.c             |    1 +
 t/t5571-pre-push-hook.sh   |  145 ++++++++++++++++++++++++++++++++++++++++++++
 transport.c                |   25 ++++++++
 transport.h                |    1 +
 5 files changed, 200 insertions(+)
 create mode 100755 t/t5571-pre-push-hook.sh
Show changes to 5 files +200 −0

Documentation/githooks.txt, builtin/push.c, t/t5571-pre-push-hook.sh, transport.c, transport.h

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b9003fe..e9539bb 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -176,6 +176,34 @@ save and restore any form of metadata associated with the working tree
 (eg: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
 for an example of how to do this.
 
+pre-push
+~~~~~~~~
+
+This hook is called by 'git push' and can be used to prevent a push from
+taking place.  The hook is called with a variable number of parameters.
+
+The first parameters provide the name and location of the destination
+remote, if a named remote is not being used both values will be the same.
+
+Remaining parameters provide information about the commits which are to be
+pushed and the ref names being used.  These arguments take the form:
+
+  <local ref>:<local sha1>:<remote ref>:<remote sha1>
+
+For instance, if the command +git push origin master:foreign+ were run the
+hook would be called with a third arugment similar to:
+
+  refs/heads/master:67890:refs/heads/foreign:12345
+
+although the full, 40-character SHA1s would be supplied.  If the foreign ref
+does not yet exist the `<remote SHA1>` will be 40 `0`.  If a ref is to be
+deleted, the `<local ref>` will be supplied as `(delete)` and the `<local
+SHA1>` will be 40 `0`.  If the local commit was specified by something other
+than a name which could be expanded (such as `HEAD~`, or a SHA1) it will be
+supplied as it was originally given.
+
+If this hook exits with a non-zero status, 'git push' will abort.
+
 [[pre-receive]]
 pre-receive
 ~~~~~~~~~~~
diff --git a/builtin/push.c b/builtin/push.c
index db9ba30..c33fb9b 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -399,6 +399,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "progress", &progress, N_("force progress reporting")),
 		OPT_BIT(0, "prune", &flags, N_("prune locally removed refs"),
 			TRANSPORT_PUSH_PRUNE),
+		OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
 		OPT_END()
 	};
 
diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh
new file mode 100755
index 0000000..5444c9b
--- /dev/null
+++ b/t/t5571-pre-push-hook.sh
@@ -0,0 +1,145 @@
+#!/bin/sh
+
+test_description='check pre-push hooks'
+. ./test-lib.sh
+
+# Setup hook that always succeeds
+HOOKDIR="$(git rev-parse --git-dir)/hooks"
+HOOK="$HOOKDIR/pre-push"
+mkdir -p "$HOOKDIR"
+cat >"$HOOK" <<EOF
+#!/bin/sh
+exit 0
+EOF
+chmod +x "$HOOK"
+
+test_expect_success 'setup' '
+	git config push.default upstream &&
+	git init --bare repo1 &&
+	git remote add parent1 repo1 &&
+	test_commit one &&
+	git push parent1 HEAD:foreign
+'
+cat >"$HOOK" <<EOF
+#!/bin/sh
+exit 1
+EOF
+
+COMMIT1="$(git rev-parse HEAD)"
+export COMMIT1
+
+test_expect_success 'push with failing hook' '
+	test_commit two &&
+	test_must_fail git push parent1 HEAD
+'
+
+test_expect_success '--no-verify bypasses hook' '
+	git push --no-verify parent1 HEAD
+'
+
+COMMIT2="$(git rev-parse HEAD)"
+export COMMIT2
+
+cat >"$HOOK" <<'EOF'
+#!/bin/sh -ex
+test "$#" = 3
+test "$1" = parent1
+test "$2" = repo1
+test "$3" = "refs/heads/master:$COMMIT2:refs/heads/foreign:$COMMIT1"
+EOF
+
+test_expect_success 'push with hook' '
+	git push parent1 master:foreign
+'
+
+test_expect_success 'add a branch' '
+	git checkout -b other &&
+	test_commit three
+'
+
+COMMIT3="$(git rev-parse HEAD)"
+export COMMIT3
+
+cat >"$HOOK" <<'EOF'
+#!/bin/sh -ex
+test "$#" = 4
+test "$1" = parent1
+test "$2" = repo1
+test "$3" = "refs/heads/other:$COMMIT3:refs/heads/foreign:$COMMIT2"
+test "$4" = "refs/heads/master:$COMMIT2:refs/heads/new:$_z40"
+EOF
+
+test_expect_success 'push multiple refs' '
+	git push parent1 other:foreign master:new
+'
+
+test_expect_success 'add a branch with an upstream' '
+	git checkout -t -b tracking parent1/foreign &&
+	test_commit four
+'
+COMMIT4="$(git rev-parse HEAD)"
+export COMMIT4
+
+cat >"$HOOK" <<'EOF'
+#!/bin/sh -ex
+test "$#" = 3
+test "$1" = parent1
+test "$2" = repo1
+test "$3" = "refs/heads/tracking:$COMMIT4:refs/heads/foreign:$COMMIT3"
+EOF
+
+test_expect_success 'push to upstream branch' '
+	git push &&
+	git checkout other
+'
+
+cat >"$HOOK" <<'EOF'
+#!/bin/sh -ex
+test "$#" = 3
+test "$1" = parent1
+test "$2" = repo1
+test "$3" = "(delete):$_z40:refs/heads/new:$COMMIT2"
+EOF
+
+test_expect_success 'push deletion' '
+	git push parent1 :new
+'
+
+cat >"$HOOK" <<'EOF'
+#!/bin/sh -ex
+test "$#" = 3
+test "$1" = repo2
+test "$2" = repo2
+test "$3" = "refs/heads/other:$COMMIT3:refs/heads/new:$_z40"
+EOF
+
+test_expect_success 'push to URL' '
+	git init --bare repo2 &&
+	git push repo2 other:new
+'
+
+ABBR=$(expr substr $COMMIT3 1 8)
+export ABBR
+
+cat >"$HOOK" <<'EOF'
+#!/bin/sh -ex
+test "$#" = 4
+test "$3" = "HEAD~:$COMMIT2:refs/heads/commitish:$_z40"
+test "$4" = "$ABBR:$COMMIT3:refs/heads/sha:$_z40"
+EOF
+
+test_expect_success 'push commit' '
+	git push parent1 HEAD~:refs/heads/commitish $ABBR:refs/heads/sha
+'
+
+cat >"$HOOK" <<'EOF'
+#!/bin/sh -ex
+test "$#" = 3
+test "$3" = "refs/tags/one:$COMMIT1:refs/tags/tagpush:$_z40"
+EOF
+
+test_expect_success 'push tag' '
+	git push parent1 one:tagpush
+'
+
+test_done
diff --git a/transport.c b/transport.c
index 9932f40..b0c9a15 100644
--- a/transport.c
+++ b/transport.c
@@ -1052,6 +1052,7 @@ int transport_push(struct transport *transport,
 		int porcelain = flags & TRANSPORT_PUSH_PORCELAIN;
 		int pretend = flags & TRANSPORT_PUSH_DRY_RUN;
 		int push_ret, ret, err;
+		char *hook;
 
 		if (flags & TRANSPORT_PUSH_ALL)
 			match_flags |= MATCH_REFS_ALL;
@@ -1069,6 +1070,30 @@ int transport_push(struct transport *transport,
 			flags & TRANSPORT_PUSH_MIRROR,
 			flags & TRANSPORT_PUSH_FORCE);
 
+		if (!(flags & TRANSPORT_PUSH_NO_HOOK) && (hook = find_hook("pre-push"))) {
+			struct ref *r;
+			struct argv_array argv = ARGV_ARRAY_INIT;
+
+			argv_array_push(&argv, hook);
+			argv_array_push(&argv, transport->remote->name);
+			argv_array_push(&argv, transport->url);
+
+			for (r = remote_refs; r; r = r->next) {
+				if (!r->peer_ref) continue;
+				if (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;
+				if (r->status == REF_STATUS_UPTODATE) continue;
+
+				argv_array_pushf(&argv, "%s:%s:%s:%s",
+					r->peer_ref->name, sha1_to_hex(r->new_sha1),
+					r->name, sha1_to_hex(r->old_sha1));
+			}
+
+			ret = run_hook_argv(NULL, argv);
+			argv_array_clear(&argv);
+			if (ret)
+				return -1;
+		}
+
 		if ((flags & TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND) && !is_bare_repository()) {
 			struct ref *ref = remote_refs;
 			for (; ref; ref = ref->next)
diff --git a/transport.h b/transport.h
index 4a61c0c..3bc9863 100644
--- a/transport.h
+++ b/transport.h
@@ -104,6 +104,7 @@ struct transport {
 #define TRANSPORT_RECURSE_SUBMODULES_CHECK 64
 #define TRANSPORT_PUSH_PRUNE 128
 #define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256
+#define TRANSPORT_PUSH_NO_HOOK 512
 
 #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
 #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)
-- 
1.7.10.4
Aaron Schrab· Dec 28, 2012, 22:57 UTC · re: Aaron Schrab · lore

[PATCH 4/4] Add sample pre-push hook script

Create a sample of a script for a pre-push hook. The main purpose is to illustrate how a script may parse the parameters which are supplied to such a hook. The script may also be useful to some people as-is for avoiding to push commits which are marked as a work in progress.

Signed-off-by: Aaron Schrab <aaron@schrab.com>
---
 templates/hooks--pre-push.sample |   63 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 63 insertions(+)
 create mode 100644 templates/hooks--pre-push.sample
Show changes to templates/hooks--pre-push.sample +63 −0
diff --git a/templates/hooks--pre-push.sample b/templates/hooks--pre-push.sample
new file mode 100644
index 0000000..1d3b4a3
--- /dev/null
+++ b/templates/hooks--pre-push.sample
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+# An example hook script to verify what is about to be pushed.
+# Called by "git push" after it has checked the remote status, but before
+# anything has been pushed.  If this script exits with a non-zero status
+# nothing will be pushed.
+#
+# This hook is called with the following parameters:
+#
+# $1 -- Name of the remote to which the push is being done
+# $2 -- URL to which the push is being done
+#
+#   If pushing without using a named remote those arguments will be equal.
+#
+# Further arguments provide information about the commits which are being
+# pushed in the form:
+#
+#   <local ref>:<local sha1>:<remote ref>:<remote sha1>
+#
+# This sample shows how to prevent push of commits where the log
+# message starts with "WIP" (work in progress).
+
+remote="$1"
+url="$2"
+shift 2
+
+z40=0000000000000000000000000000000000000000
+
+old_ifs="$IFS"
+for to_push in "$@"
+do
+	# Split the value into its parts
+	IFS=:
+	set -- $to_push
+	IFS="$old_ifs"
+
+	local_ref="$1"
+	local_sha="$2"
+	remote_ref="$3"
+	remote_sha="$4"
+
+	if [ "$local_sha" = $z40 ]
+	then
+		range=''
+		# Handle deletes
+	else
+		if [ "$remote_sha" = $z40 ]
+		then
+			range="$local_sha"
+		else
+			range="$remote_sha..$local_sha"
+		fi
+
+		commit=`git rev-list -n 1 --grep '^WIP' "$range"`
+		if [ -n "$commit" ]
+		then
+			echo "Found WIP commit in $local_ref, not pushing"
+			exit 1
+		fi
+	fi
+done
+
+exit 0
-- 
1.7.10.4
Junio C Hamano· Dec 29, 2012, 02:01 UTC · re: Aaron Schrab · lore

Re: [PATCH 0/4] pre-push hook support

Aaron Schrab <aaron@schrab.com> writes:
Show 17 quoted lines
> There have been at least a couple of submissions to add support for a
> pre-push hook, which were rejected at least partially because they didn't
> provide enough information to a hook script for it to determine what was
> to be pushed any better than a separate wrapper around the 'git push'
> command would be able to do.  In this series I attempt to address that
> problem.
>
> The first two patches in this series do a little bit of refactoring in
> order to make it easier to call hooks with a variable number of arguments.
>
> The third patch actually adds support for calling a pre-push hook.  If it
> exists, it will be called with the name and URL of the destination remote
> (if a named remote isn't being used, the URL will be supplied for both)
> followed by another argument for each ref being pushed; these arguments
> take the form:
>
>   <local ref>:<local sha1>:<remote ref>:<remote sha1>

One lesson we learned long time ago while doing hooks is to avoid unbound number of command line arguments and instead feed them from the standard input. I think this should do the same.

> This should provide enough information for a script to easily determine
> the set of commits that is being pushed, and thus make a decision if that
> should be allowed.
How does the hook communicate its decision to the calling Git?
Will it be "all-or-none", or "I'll allow these but not those"?
Aaron Schrab· Dec 29, 2012, 14:50 UTC · re: Junio C Hamano · lore

Re: [PATCH 0/4] pre-push hook support

At 18:01 -0800 28 Dec 2012, Junio C Hamano <gitster@pobox.com> wrote:
>One lesson we learned long time ago while doing hooks is to avoid
>unbound number of command line arguments and instead feed them from
>the standard input.  I think this should do the same.

Good point. I had been trying to keep the interface for this hook as close as possible to the ones for other client-side hooks on the theory that less development effort may go into those than for server-side hooks. But thinking on that more I certainly see that this could easily run into limits on argument length on some systems; especially when it's likely that each of those arguments is likely to be over 100 bytes long.

I'll work on an updated version which sends the variable length information over a pipe, using the command-line arguments only to pass the remote name and URL.

>How does the hook communicate its decision to the calling Git?
>
>Will it be "all-or-none", or "I'll allow these but not those"?

Currently it just uses the exit code to communicate that back, so it's all-or-none. I think I'll keep that in the updated version as well.

A future enhancement could modify the protocol to support reading from the hook's stdout the names of remote refs which are to be rejected, I think that just having the option for all-or-nothing is a good starting point.

Junio C Hamano· Dec 29, 2012, 16:48 UTC · re: Aaron Schrab · lore

Re: [PATCH 0/4] pre-push hook support

Aaron Schrab <aaron@schrab.com> writes:
Show 5 quoted lines
> At 18:01 -0800 28 Dec 2012, Junio C Hamano <gitster@pobox.com> wrote:
>>Will it be "all-or-none", or "I'll allow these but not those"?
>
> Currently it just uses the exit code to communicate that back, so it's
> all-or-none.  I think I'll keep that in the updated version as well.
Thanks; that sounds sensible.
Aaron Schrab· Jan 13, 2013, 05:17 UTC · re: Aaron Schrab · lore

[PATCH v2 0/3] pre-push hook support

Main changes since the initial version:
 * The first patch converts the existing hook callers to use the new
   find_hook() function.
 * Information about what is to be pushed is now sent over a pipe rather
   than passed as command-line parameters.
Aaron Schrab (3):
  hooks: Add function to check if a hook exists
  push: Add support for pre-push hooks
  Add sample pre-push hook script
 Documentation/githooks.txt       |  29 +++++++++
 builtin/commit.c                 |   6 +-
 builtin/push.c                   |   1 +
 builtin/receive-pack.c           |  25 ++++----
 run-command.c                    |  15 ++++-
 run-command.h                    |   1 +
 t/t5571-pre-push-hook.sh         | 129 +++++++++++++++++++++++++++++++++++++++
 templates/hooks--pre-push.sample |  53 ++++++++++++++++
 transport.c                      |  60 ++++++++++++++++++
 transport.h                      |   1 +
 10 files changed, 300 insertions(+), 20 deletions(-)
 create mode 100755 t/t5571-pre-push-hook.sh
 create mode 100644 templates/hooks--pre-push.sample
-- 
1.8.1.340.g425b78d
Aaron Schrab· Jan 13, 2013, 05:17 UTC · re: Aaron Schrab · lore

[PATCH v2 1/3] hooks: Add function to check if a hook exists

Create find_hook() function to determine if a given hook exists and is executable. If it is, the path to the script will be returned, otherwise NULL is returned.

This encapsulates the tests that are used to check for the existence of a hook in one place, making it easier to modify those checks if that is found to be necessary. This also makes it simple for places that can use a hook to check if a hook exists before doing, possibly lengthy, setup work which would be pointless if no such hook is present.

The returned value is left as a static value from get_pathname() rather than a duplicate because it is anticipated that the return value will either be used as a boolean, immediately added to an argv_array list which would result in it being duplicated at that point, or used to actually run the command without much intervening work. Callers which need to hold onto the returned value for a longer time are expected to duplicate the return value themselves.

Signed-off-by: Aaron Schrab <aaron@schrab.com>
---
 builtin/commit.c       |  6 ++----
 builtin/receive-pack.c | 25 +++++++++++--------------
 run-command.c          | 15 +++++++++++++--
 run-command.h          |  1 +
 4 files changed, 27 insertions(+), 20 deletions(-)
Show changes to 4 files +27 −20

builtin/commit.c, builtin/receive-pack.c, run-command.c, run-command.h

diff --git a/builtin/commit.c b/builtin/commit.c
index d6dd3df..65d08d2 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -1327,8 +1327,6 @@ static int git_commit_config(const char *k, const char *v, void *cb)
 	return git_status_config(k, v, s);
 }
 
-static const char post_rewrite_hook[] = "hooks/post-rewrite";
-
 static int run_rewrite_hook(const unsigned char *oldsha1,
 			    const unsigned char *newsha1)
 {
@@ -1339,10 +1337,10 @@ static int run_rewrite_hook(const unsigned char *oldsha1,
 	int code;
 	size_t n;
 
-	if (access(git_path(post_rewrite_hook), X_OK) < 0)
+	argv[0] = find_hook("post-rewrite");
+	if (!argv[0])
 		return 0;
 
-	argv[0] = git_path(post_rewrite_hook);
 	argv[1] = "amend";
 	argv[2] = NULL;
 
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index ff781fe..e8878de 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -182,9 +182,6 @@ struct command {
 	char ref_name[FLEX_ARRAY]; /* more */
 };
 
-static const char pre_receive_hook[] = "hooks/pre-receive";
-static const char post_receive_hook[] = "hooks/post-receive";
-
 static void rp_error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 static void rp_warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 
@@ -242,10 +239,10 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta
 	const char *argv[2];
 	int code;
 
-	if (access(hook_name, X_OK) < 0)
+	argv[0] = find_hook(hook_name);
+	if (!argv[0])
 		return 0;
 
-	argv[0] = hook_name;
 	argv[1] = NULL;
 
 	memset(&proc, 0, sizeof(proc));
@@ -331,15 +328,14 @@ static int run_receive_hook(struct command *commands, const char *hook_name,
 
 static int run_update_hook(struct command *cmd)
 {
-	static const char update_hook[] = "hooks/update";
 	const char *argv[5];
 	struct child_process proc;
 	int code;
 
-	if (access(update_hook, X_OK) < 0)
+	argv[0] = find_hook("update");
+	if (!argv[0])
 		return 0;
 
-	argv[0] = update_hook;
 	argv[1] = cmd->ref_name;
 	argv[2] = sha1_to_hex(cmd->old_sha1);
 	argv[3] = sha1_to_hex(cmd->new_sha1);
@@ -532,24 +528,25 @@ static const char *update(struct command *cmd)
 	}
 }
 
-static char update_post_hook[] = "hooks/post-update";
-
 static void run_update_post_hook(struct command *commands)
 {
 	struct command *cmd;
 	int argc;
 	const char **argv;
 	struct child_process proc;
+	char *hook;
 
+	hook = find_hook("post-update");
 	for (argc = 0, cmd = commands; cmd; cmd = cmd->next) {
 		if (cmd->error_string || cmd->did_not_exist)
 			continue;
 		argc++;
 	}
-	if (!argc || access(update_post_hook, X_OK) < 0)
+	if (!argc || !hook)
 		return;
+
 	argv = xmalloc(sizeof(*argv) * (2 + argc));
-	argv[0] = update_post_hook;
+	argv[0] = hook;
 
 	for (argc = 1, cmd = commands; cmd; cmd = cmd->next) {
 		char *p;
@@ -704,7 +701,7 @@ static void execute_commands(struct command *commands, const char *unpacker_erro
 				       0, &cmd))
 		set_connectivity_errors(commands);
 
-	if (run_receive_hook(commands, pre_receive_hook, 0)) {
+	if (run_receive_hook(commands, "pre-receive", 0)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
 				cmd->error_string = "pre-receive hook declined";
@@ -994,7 +991,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_hook, 1);
+		run_receive_hook(commands, "post-receive", 1);
 		run_update_post_hook(commands);
 		if (auto_gc) {
 			const char *argv_gc_auto[] = {
diff --git a/run-command.c b/run-command.c
index 0471219..12d4ddb 100644
--- a/run-command.c
+++ b/run-command.c
@@ -735,6 +735,15 @@ int finish_async(struct async *async)
 #endif
 }
 
+char *find_hook(const char *name)
+{
+	char *path = git_path("hooks/%s", name);
+	if (access(path, X_OK) < 0)
+		path = NULL;
+
+	return path;
+}
+
 int run_hook(const char *index_file, const char *name, ...)
 {
 	struct child_process hook;
@@ -744,11 +753,13 @@ int run_hook(const char *index_file, const char *name, ...)
 	va_list args;
 	int ret;
 
-	if (access(git_path("hooks/%s", name), X_OK) < 0)
+	p = find_hook(name);
+	if (!p)
 		return 0;
 
+	argv_array_push(&argv, p);
+
 	va_start(args, name);
-	argv_array_push(&argv, git_path("hooks/%s", name));
 	while ((p = va_arg(args, const char *)))
 		argv_array_push(&argv, p);
 	va_end(args);
diff --git a/run-command.h b/run-command.h
index 850c638..221ce33 100644
--- a/run-command.h
+++ b/run-command.h
@@ -45,6 +45,7 @@ int start_command(struct child_process *);
 int finish_command(struct child_process *);
 int run_command(struct child_process *);
 
+extern char *find_hook(const char *name);
 extern int run_hook(const char *index_file, const char *name, ...);
 
 #define RUN_COMMAND_NO_STDIN 1
-- 
1.8.1.340.g425b78d
Aaron Schrab· Jan 13, 2013, 05:17 UTC · re: Aaron Schrab · lore

[PATCH v2 2/3] push: Add support for pre-push hooks

Add support for a pre-push hook which can be used to determine if the set of refs to be pushed is suitable for the target repository. The hook is run with two arguments specifying the name and location of the destination repository.

Information about what is to be pushed is provided by sending lines of the following form to the hook's standard input:

  <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
If the hook exits with a non-zero status, the push will be aborted.

This will allow the script to determine if the push is acceptable based on the target repository and branch(es), the commits which are to be pushed, and even the source branches in some cases.

Signed-off-by: Aaron Schrab <aaron@schrab.com>
---
 Documentation/githooks.txt |  29 ++++++++++
 builtin/push.c             |   1 +
 t/t5571-pre-push-hook.sh   | 129 +++++++++++++++++++++++++++++++++++++++++++++
 transport.c                |  60 +++++++++++++++++++++
 transport.h                |   1 +
 5 files changed, 220 insertions(+)
 create mode 100755 t/t5571-pre-push-hook.sh
Show changes to 5 files +220 −0

Documentation/githooks.txt, builtin/push.c, t/t5571-pre-push-hook.sh, transport.c, transport.h

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b9003fe..d839233 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -176,6 +176,35 @@ save and restore any form of metadata associated with the working tree
 (eg: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
 for an example of how to do this.
 
+pre-push
+~~~~~~~~
+
+This hook is called by 'git push' and can be used to prevent a push from taking
+place.  The hook is called with two parameters which provide the name and
+location of the destination remote, if a named remote is not being used both
+values will be the same.
+
+Information about what is to be pushed is provided on the hook's standard
+input with lines of the form:
+
+  <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
+
+For instance, if the command +git push origin master:foreign+ were run the
+hook would receive a line like the following:
+
+  refs/heads/master 67890 refs/heads/foreign 12345
+
+although the full, 40-character SHA1s would be supplied.  If the foreign ref
+does not yet exist the `<remote SHA1>` will be 40 `0`.  If a ref is to be
+deleted, the `<local ref>` will be supplied as `(delete)` and the `<local
+SHA1>` will be 40 `0`.  If the local commit was specified by something other
+than a name which could be expanded (such as `HEAD~`, or a SHA1) it will be
+supplied as it was originally given.
+
+If this hook exits with a non-zero status, 'git push' will abort without
+pushing anything.  Information about why the push is rejected may be sent
+to the user by writing to standard error.
+
 [[pre-receive]]
 pre-receive
 ~~~~~~~~~~~
diff --git a/builtin/push.c b/builtin/push.c
index 8491e43..b158028 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -407,6 +407,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "progress", &progress, N_("force progress reporting")),
 		OPT_BIT(0, "prune", &flags, N_("prune locally removed refs"),
 			TRANSPORT_PUSH_PRUNE),
+		OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
 		OPT_END()
 	};
 
diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh
new file mode 100755
index 0000000..d68fed7
--- /dev/null
+++ b/t/t5571-pre-push-hook.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='check pre-push hooks'
+. ./test-lib.sh
+
+# Setup hook that always succeeds
+HOOKDIR="$(git rev-parse --git-dir)/hooks"
+HOOK="$HOOKDIR/pre-push"
+mkdir -p "$HOOKDIR"
+write_script "$HOOK" <<EOF
+exit 0
+EOF
+
+test_expect_success 'setup' '
+	git config push.default upstream &&
+	git init --bare repo1 &&
+	git remote add parent1 repo1 &&
+	test_commit one &&
+	git push parent1 HEAD:foreign
+'
+write_script "$HOOK" <<EOF
+exit 1
+EOF
+
+COMMIT1="$(git rev-parse HEAD)"
+export COMMIT1
+
+test_expect_success 'push with failing hook' '
+	test_commit two &&
+	test_must_fail git push parent1 HEAD
+'
+
+test_expect_success '--no-verify bypasses hook' '
+	git push --no-verify parent1 HEAD
+'
+
+COMMIT2="$(git rev-parse HEAD)"
+export COMMIT2
+
+write_script "$HOOK" <<'EOF'
+echo "$1" >actual
+echo "$2" >>actual
+cat >>actual
+EOF
+
+cat >expected <<EOF
+parent1
+repo1
+refs/heads/master $COMMIT2 refs/heads/foreign $COMMIT1
+EOF
+
+test_expect_success 'push with hook' '
+	git push parent1 master:foreign &&
+	diff expected actual
+'
+
+test_expect_success 'add a branch' '
+	git checkout -b other parent1/foreign &&
+	test_commit three
+'
+
+COMMIT3="$(git rev-parse HEAD)"
+export COMMIT3
+
+cat >expected <<EOF
+parent1
+repo1
+refs/heads/other $COMMIT3 refs/heads/foreign $COMMIT2
+EOF
+
+test_expect_success 'push to default' '
+	git push &&
+	diff expected actual
+'
+
+cat >expected <<EOF
+parent1
+repo1
+refs/tags/one $COMMIT1 refs/tags/tag1 $_z40
+HEAD~ $COMMIT2 refs/heads/prev $_z40
+EOF
+
+test_expect_success 'push non-branches' '
+	git push parent1 one:tag1 HEAD~:refs/heads/prev &&
+	diff expected actual
+'
+
+cat >expected <<EOF
+parent1
+repo1
+(delete) $_z40 refs/heads/prev $COMMIT2
+EOF
+
+test_expect_success 'push delete' '
+	git push parent1 :prev &&
+	diff expected actual
+'
+
+cat >expected <<EOF
+repo1
+repo1
+HEAD $COMMIT3 refs/heads/other $_z40
+EOF
+
+test_expect_success 'push to URL' '
+	git push repo1 HEAD &&
+	diff expected actual
+'
+
+# Test that filling pipe buffers doesn't cause failure
+# Too slow to leave enabled for general use
+if false
+then
+	printf 'parent1\nrepo1\n' >expected
+	nr=1000
+	while test $nr -lt 2000
+	do
+		nr=$(( $nr + 1 ))
+		git branch b/$nr $COMMIT3
+		echo "refs/heads/b/$nr $COMMIT3 refs/heads/b/$nr $_z40" >>expected
+	done
+
+	test_expect_success 'push many refs' '
+		git push parent1 "refs/heads/b/*:refs/heads/b/*" &&
+		diff expected actual
+	'
+fi
+
+test_done
diff --git a/transport.c b/transport.c
index 2673d27..0750a5f 100644
--- a/transport.c
+++ b/transport.c
@@ -1034,6 +1034,62 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)
 	die("Aborting.");
 }
 
+static int run_pre_push_hook(struct transport *transport,
+			     struct ref *remote_refs)
+{
+	int ret = 0, x;
+	struct ref *r;
+	struct child_process proc;
+	struct strbuf buf;
+	const char *argv[4];
+
+	if (!(argv[0] = find_hook("pre-push")))
+		return 0;
+
+	argv[1] = transport->remote->name;
+	argv[2] = transport->url;
+	argv[3] = NULL;
+
+	memset(&proc, 0, sizeof(proc));
+	proc.argv = argv;
+	proc.in = -1;
+
+	if (start_command(&proc)) {
+		finish_command(&proc);
+		return -1;
+	}
+
+	strbuf_init(&buf, 256);
+
+	for (r = remote_refs; r; r = r->next) {
+		if (!r->peer_ref) continue;
+		if (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;
+		if (r->status == REF_STATUS_UPTODATE) continue;
+
+		strbuf_reset(&buf);
+		strbuf_addf( &buf, "%s %s %s %s\n",
+			 r->peer_ref->name, sha1_to_hex(r->new_sha1),
+			 r->name, sha1_to_hex(r->old_sha1));
+
+		if (write_in_full(proc.in, buf.buf, buf.len) != buf.len) {
+			ret = -1;
+			break;
+		}
+	}
+
+	strbuf_release(&buf);
+
+	x = close(proc.in);
+	if (!ret)
+		ret = x;
+
+	x = finish_command(&proc);
+	if (!ret)
+		ret = x;
+
+	return ret;
+}
+
 int transport_push(struct transport *transport,
 		   int refspec_nr, const char **refspec, int flags,
 		   unsigned int *reject_reasons)
@@ -1074,6 +1130,10 @@ int transport_push(struct transport *transport,
 			flags & TRANSPORT_PUSH_MIRROR,
 			flags & TRANSPORT_PUSH_FORCE);
 
+		if (!(flags & TRANSPORT_PUSH_NO_HOOK))
+			if (run_pre_push_hook(transport, remote_refs))
+				return -1;
+
 		if ((flags & TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND) && !is_bare_repository()) {
 			struct ref *ref = remote_refs;
 			for (; ref; ref = ref->next)
diff --git a/transport.h b/transport.h
index bfd2df5..ac5a9f5 100644
--- a/transport.h
+++ b/transport.h
@@ -104,6 +104,7 @@ struct transport {
 #define TRANSPORT_RECURSE_SUBMODULES_CHECK 64
 #define TRANSPORT_PUSH_PRUNE 128
 #define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256
+#define TRANSPORT_PUSH_NO_HOOK 512
 
 #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
 #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)
-- 
1.8.1.340.g425b78d
Junio C Hamano· Jan 14, 2013, 17:39 UTC · re: Aaron Schrab · lore

Re: [PATCH v2 2/3] push: Add support for pre-push hooks

Aaron Schrab <aaron@schrab.com> writes:
Show 48 quoted lines
> Add support for a pre-push hook which can be used to determine if the
> set of refs to be pushed is suitable for the target repository.  The
> hook is run with two arguments specifying the name and location of the
> destination repository.
>
> Information about what is to be pushed is provided by sending lines of
> the following form to the hook's standard input:
>
>   <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
>
> If the hook exits with a non-zero status, the push will be aborted.
>
> This will allow the script to determine if the push is acceptable based
> on the target repository and branch(es), the commits which are to be
> pushed, and even the source branches in some cases.
>
> Signed-off-by: Aaron Schrab <aaron@schrab.com>
> ---
>  Documentation/githooks.txt |  29 ++++++++++
>  builtin/push.c             |   1 +
>  t/t5571-pre-push-hook.sh   | 129 +++++++++++++++++++++++++++++++++++++++++++++
>  transport.c                |  60 +++++++++++++++++++++
>  transport.h                |   1 +
>  5 files changed, 220 insertions(+)
>  create mode 100755 t/t5571-pre-push-hook.sh
>
> diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
> index b9003fe..d839233 100644
> --- a/Documentation/githooks.txt
> +++ b/Documentation/githooks.txt
> @@ -176,6 +176,35 @@ save and restore any form of metadata associated with the working tree
>  (eg: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
>  for an example of how to do this.
>  
> +pre-push
> +~~~~~~~~
> +
> +This hook is called by 'git push' and can be used to prevent a push from taking
> +place.  The hook is called with two parameters which provide the name and
> +location of the destination remote, if a named remote is not being used both
> +values will be the same.
> +
> +Information about what is to be pushed is provided on the hook's standard
> +input with lines of the form:
> +
> +  <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
> +
> +For instance, if the command +git push origin master:foreign+ were run the

Just being curious, but why use +monospace text+ here? Most of the new text use `monospace text literally` instead in this patch.

Show 5 quoted lines
> +hook would receive a line like the following:
> +
> +  refs/heads/master 67890 refs/heads/foreign 12345
> +
> +although the full, 40-character SHA1s would be supplied.
Perhaps ellipses are called for here?
    refs/heads/master 67890... refs/heads/foreign 12345...
 (the above abbreviates full 40-hexdigits for illustration purposes only)
Show 10 quoted lines
> +If the foreign ref
> +does not yet exist the `<remote SHA1>` will be 40 `0`.  If a ref is to be
> +deleted, the `<local ref>` will be supplied as `(delete)` and the `<local
> +SHA1>` will be 40 `0`.  If the local commit was specified by something other
> +than a name which could be expanded (such as `HEAD~`, or a SHA1) it will be
> +supplied as it was originally given.
> +
> +If this hook exits with a non-zero status, 'git push' will abort without
> +pushing anything.  Information about why the push is rejected may be sent
> +to the user by writing to standard error.

s/standard error/& of the hook/; perhaps? It is unclear who does the writing and it can be misunderstood that git-push will write to standard error upon seeing your hook that silently exits.

Show 11 quoted lines
> diff --git a/builtin/push.c b/builtin/push.c
> index 8491e43..b158028 100644
> --- a/builtin/push.c
> +++ b/builtin/push.c
> @@ -407,6 +407,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
>  		OPT_BOOL(0, "progress", &progress, N_("force progress reporting")),
>  		OPT_BIT(0, "prune", &flags, N_("prune locally removed refs"),
>  			TRANSPORT_PUSH_PRUNE),
> +		OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
>  		OPT_END()
>  	};

So to countermand this, you have to say --no-no-verify? Wouldn't it be more natural to introduce a --verify option that turns the bit on, which automatically gives you --no-verify to turn it off? A bit in a flag word can be initialized to true before the flag word is given to the parse_options() machinery to make the field default to true, no?

Junio C Hamano· Jan 15, 2013, 00:36 UTC · re: Aaron Schrab · lore

Re: [PATCH v2 2/3] push: Add support for pre-push hooks

Aaron Schrab <aaron@schrab.com> writes:
Show 19 quoted lines
>  t/t5571-pre-push-hook.sh   | 129 +++++++++++++++++++++++++++++++++++++++++++++
> diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh
> new file mode 100755
> index 0000000..d68fed7
> --- /dev/null
> +++ b/t/t5571-pre-push-hook.sh
> @@ -0,0 +1,129 @@
> +#!/bin/sh
> +
> +test_description='check pre-push hooks'
> +. ./test-lib.sh
> +
> +# Setup hook that always succeeds
> +HOOKDIR="$(git rev-parse --git-dir)/hooks"
> +HOOK="$HOOKDIR/pre-push"
> +mkdir -p "$HOOKDIR"
> +write_script "$HOOK" <<EOF
> +exit 0
> +EOF

As this script is expected to read from the pipe, if this exits before the parent has a chance to write to the pipe, the parent can be killed with sigpipe.

At least the attached patch is necessary.

In the longer term, we may want to discuss what should happen when the hook exited without even reading what we fed. My gut feeling is that we can still trust its exit status (a hook that was badly coded so it wanted to read from us and use that information to decide but somehow died before fully reading from us is not likely to exit with zero status, so we wouldn't diagnosing breakage as a success), but there may be downsides for being that lax.

If we decide we want to be lax, then the call site of this hook and the pre-receive hook (is there any other "take info from the standard input" hook?) need to be modified so that they ignore sigpipe, I think.

There was a related discussion around this issue about a year ago.
http://thread.gmane.org/gmane.comp.version-control.git/180346/focus=186291
 t/t5571-pre-push-hook.sh | 3 +++
 1 file changed, 3 insertions(+)
Show changes to t/t5571-pre-push-hook.sh +3 −0
diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh
index d68fed7..577d252 100755
--- a/t/t5571-pre-push-hook.sh
+++ b/t/t5571-pre-push-hook.sh
@@ -8,6 +8,7 @@ HOOKDIR="$(git rev-parse --git-dir)/hooks"
 HOOK="$HOOKDIR/pre-push"
 mkdir -p "$HOOKDIR"
 write_script "$HOOK" <<EOF
+cat >/dev/null
 exit 0
 EOF
 
@@ -19,6 +20,7 @@ test_expect_success 'setup' '
 	git push parent1 HEAD:foreign
 '
 write_script "$HOOK" <<EOF
+cat >/dev/null
 exit 1
 EOF
 
@@ -38,6 +40,7 @@ COMMIT2="$(git rev-parse HEAD)"
 export COMMIT2
 
 write_script "$HOOK" <<'EOF'
+cat >/dev/null
 echo "$1" >actual
 echo "$2" >>actual
 cat >>actual
Junio C Hamano· Jan 15, 2013, 03:12 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 2/3] push: Add support for pre-push hooks

Junio C Hamano <gitster@pobox.com> writes:
> At least the attached patch is necessary.
Sorry, but the last hunk (see below) is not.  It breaks the hook.
Show 18 quoted lines
> In the longer term, we may want to discuss what should happen when
> the hook exited without even reading what we fed.  My gut feeling is
> that we can still trust its exit status (a hook that was badly coded
> so it wanted to read from us and use that information to decide but
> somehow died before fully reading from us is not likely to exit with
> zero status, so we wouldn't diagnosing breakage as a success), but
> there may be downsides for being that lax.
>
> If we decide we want to be lax, then the call site of this hook and
> the pre-receive hook (is there any other "take info from the
> standard input" hook?) need to be modified so that they ignore
> sigpipe, I think.
>
> There was a related discussion around this issue about a year ago.
>
> http://thread.gmane.org/gmane.comp.version-control.git/180346/focus=186291
> ...
>
Show 8 quoted lines
> @@ -38,6 +40,7 @@ COMMIT2="$(git rev-parse HEAD)"
>  export COMMIT2
>  
>  write_script "$HOOK" <<'EOF'
> +cat >/dev/null
>  echo "$1" >actual
>  echo "$2" >>actual
>  cat >>actual

As this one wants to keep the incoming data to "actual", we do not want the extra "cat" to slurp everything in. Sorry for not being careful.

Aaron Schrab· Jan 13, 2013, 05:17 UTC · re: Aaron Schrab · lore

[PATCH v2 3/3] Add sample pre-push hook script

Create a sample of a script for a pre-push hook. The main purpose is to illustrate how a script may parse the information which is supplied to such a hook. The script may also be useful to some people as-is for avoiding to push commits which are marked as a work in progress.

Signed-off-by: Aaron Schrab <aaron@schrab.com>
---
 templates/hooks--pre-push.sample | 53 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 53 insertions(+)
 create mode 100644 templates/hooks--pre-push.sample
Show changes to templates/hooks--pre-push.sample +53 −0
diff --git a/templates/hooks--pre-push.sample b/templates/hooks--pre-push.sample
new file mode 100644
index 0000000..15ab6d8
--- /dev/null
+++ b/templates/hooks--pre-push.sample
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+# An example hook script to verify what is about to be pushed.  Called by "git
+# push" after it has checked the remote status, but before anything has been
+# pushed.  If this script exits with a non-zero status nothing will be pushed.
+#
+# This hook is called with the following parameters:
+#
+# $1 -- Name of the remote to which the push is being done
+# $2 -- URL to which the push is being done
+#
+# If pushing without using a named remote those arguments will be equal.
+#
+# Information about the commits which are being pushed is supplied as lines to
+# the standard input in the form:
+#
+#   <local ref> <local sha1> <remote ref> <remote sha1>
+#
+# This sample shows how to prevent push of commits where the log message starts
+# with "WIP" (work in progress).
+
+remote="$1"
+url="$2"
+
+z40=0000000000000000000000000000000000000000
+
+IFS=' '
+while read local_ref local_sha remote_ref remote_sha
+do
+	if [ "$local_sha" = $z40 ]
+	then
+		# Handle delete
+	else
+		if [ "$remote_sha" = $z40 ]
+		then
+			# New branch, examine all commits
+			range="$local_sha"
+		else
+			# Update to existing branch, examine new commits
+			range="$remote_sha..$local_sha"
+		fi
+
+		# Check for WIP commit
+		commit=`git rev-list -n 1 --grep '^WIP' "$range"`
+		if [ -n "$commit" ]
+		then
+			echo "Found WIP commit in $local_ref, not pushing"
+			exit 1
+		fi
+	fi
+done
+
+exit 0
-- 
1.8.1.340.g425b78d
Junio C Hamano· Jan 14, 2013, 17:42 UTC · re: Aaron Schrab · lore

Re: [PATCH v2 3/3] Add sample pre-push hook script

Aaron Schrab <aaron@schrab.com> writes:
Show 37 quoted lines
> Create a sample of a script for a pre-push hook.  The main purpose is to
> illustrate how a script may parse the information which is supplied to
> such a hook.  The script may also be useful to some people as-is for
> avoiding to push commits which are marked as a work in progress.
>
> Signed-off-by: Aaron Schrab <aaron@schrab.com>
> ---
>  templates/hooks--pre-push.sample | 53 ++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 53 insertions(+)
>  create mode 100644 templates/hooks--pre-push.sample
>
> diff --git a/templates/hooks--pre-push.sample b/templates/hooks--pre-push.sample
> new file mode 100644
> index 0000000..15ab6d8
> --- /dev/null
> +++ b/templates/hooks--pre-push.sample
> @@ -0,0 +1,53 @@
> +#!/bin/sh
> +
> +# An example hook script to verify what is about to be pushed.  Called by "git
> +# push" after it has checked the remote status, but before anything has been
> +# pushed.  If this script exits with a non-zero status nothing will be pushed.
> +#
> +# This hook is called with the following parameters:
> +#
> +# $1 -- Name of the remote to which the push is being done
> +# $2 -- URL to which the push is being done
> +#
> +# If pushing without using a named remote those arguments will be equal.
> +#
> +# Information about the commits which are being pushed is supplied as lines to
> +# the standard input in the form:
> +#
> +#   <local ref> <local sha1> <remote ref> <remote sha1>
> +#
> +# This sample shows how to prevent push of commits where the log message starts
> +# with "WIP" (work in progress).

An example for a plausible use case is nice to have. I would prefer to see any new shell script to follow the Git style, though.

Show 11 quoted lines
> +remote="$1"
> +url="$2"
> +
> +z40=0000000000000000000000000000000000000000
> +
> +IFS=' '
> +while read local_ref local_sha remote_ref remote_sha
> +do
> +	if [ "$local_sha" = $z40 ]
> +	then
> +		# Handle delete
    ... by doing what?
Show 21 quoted lines
> +	else
> +		if [ "$remote_sha" = $z40 ]
> +		then
> +			# New branch, examine all commits
> +			range="$local_sha"
> +		else
> +			# Update to existing branch, examine new commits
> +			range="$remote_sha..$local_sha"
> +		fi
> +
> +		# Check for WIP commit
> +		commit=`git rev-list -n 1 --grep '^WIP' "$range"`
> +		if [ -n "$commit" ]
> +		then
> +			echo "Found WIP commit in $local_ref, not pushing"
> +			exit 1
> +		fi
> +	fi
> +done
> +
> +exit 0
Junio C Hamano· Jan 14, 2013, 17:42 UTC · re: Aaron Schrab · lore

Re: [PATCH v2 0/3] pre-push hook support

Aaron Schrab <aaron@schrab.com> writes:
Show 11 quoted lines
> Main changes since the initial version:
>
>  * The first patch converts the existing hook callers to use the new
>    find_hook() function.
>  * Information about what is to be pushed is now sent over a pipe rather
>    than passed as command-line parameters.
>
> Aaron Schrab (3):
>   hooks: Add function to check if a hook exists
>   push: Add support for pre-push hooks
>   Add sample pre-push hook script
Getting much nicer.  Thanks.
Junio C Hamano· Jan 14, 2013, 22:54 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 0/3] pre-push hook support

Junio C Hamano <gitster@pobox.com> writes:
Show 15 quoted lines
> Aaron Schrab <aaron@schrab.com> writes:
>
>> Main changes since the initial version:
>>
>>  * The first patch converts the existing hook callers to use the new
>>    find_hook() function.
>>  * Information about what is to be pushed is now sent over a pipe rather
>>    than passed as command-line parameters.
>>
>> Aaron Schrab (3):
>>   hooks: Add function to check if a hook exists
>>   push: Add support for pre-push hooks
>>   Add sample pre-push hook script
>
> Getting much nicer.  Thanks.

Hmph, t5571 seems to be flaky in that it sometimes fails but passes when run again. Something timing dependent is going on???

Junio C Hamano· Jan 15, 2013, 00:24 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 0/3] pre-push hook support

Junio C Hamano <gitster@pobox.com> writes:
Show 20 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
>
>> Aaron Schrab <aaron@schrab.com> writes:
>>
>>> Main changes since the initial version:
>>>
>>>  * The first patch converts the existing hook callers to use the new
>>>    find_hook() function.
>>>  * Information about what is to be pushed is now sent over a pipe rather
>>>    than passed as command-line parameters.
>>>
>>> Aaron Schrab (3):
>>>   hooks: Add function to check if a hook exists
>>>   push: Add support for pre-push hooks
>>>   Add sample pre-push hook script
>>
>> Getting much nicer.  Thanks.
>
> Hmph, t5571 seems to be flaky in that it sometimes fails but passes
> when run again.  Something timing dependent is going on???
With this patch applied, repeatedly try to
 - make sure "foreign" ref does not exist; and
 - attempt pushing the HEAD:foreign to create the "foreign" ref

until it fails, I can get it stop before the output scrolls off of my 114 line terminal. Then when I revert the changes to transport.[ch] and builtin/push.c in this series, the test will keep going.

Wait. The sample hook used in the test _is_ fed some input but it exits without reading any. What happens when we fork it, and it completes execution before we even have a chance to feed a single byte? Wont' we get a sigpipe and die?

Yup, I think that is what is missing from run_pre_push_hook() implementation.

Show changes to t/t5571-pre-push-hook.sh +8 −1
diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh
index d68fed7..050318b 100755
--- a/t/t5571-pre-push-hook.sh
+++ b/t/t5571-pre-push-hook.sh
@@ -16,8 +16,15 @@ test_expect_success 'setup' '
 	git init --bare repo1 &&
 	git remote add parent1 repo1 &&
 	test_commit one &&
-	git push parent1 HEAD:foreign
+	while :
+	do
+		git push parent1 :refs/heads/foreign &&
+		git push parent1 HEAD:foreign || break
+	done
 '
+
+exit
+
 write_script "$HOOK" <<EOF
 exit 1
 EOF

← back to recent threads