From 5a7da2dca166fab74f4514697e26dd80e79933f5 Mon Sep 17 00:00:00 2001 From: Aaron Schrab Date: Sun, 13 Jan 2013 00:17:02 -0500 Subject: 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 Signed-off-by: Junio C Hamano 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 -- cgit v0.10.2-6-g49f6 From ec55559f937727bcb0fa8a3dfe6af68c188e968a Mon Sep 17 00:00:00 2001 From: Aaron Schrab Date: Sun, 13 Jan 2013 00:17:03 -0500 Subject: 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: SP SP SP 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 Signed-off-by: Junio C Hamano 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: + + SP SP SP 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 `` will be 40 `0`. If a ref is to be +deleted, the `` will be supplied as `(delete)` and the `` 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..6f9916a --- /dev/null +++ b/t/t5571-pre-push-hook.sh @@ -0,0 +1,131 @@ +#!/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" </dev/null +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" </dev/null +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 <expected <expected <expected <expected <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) -- cgit v0.10.2-6-g49f6 From 87c86dd14abe8db7d00b0df5661ef8cf147a72a3 Mon Sep 17 00:00:00 2001 From: Aaron Schrab Date: Sun, 13 Jan 2013 00:17:04 -0500 Subject: 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 Signed-off-by: Junio C Hamano 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: +# +# +# +# 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 -- cgit v0.10.2-6-g49f6