Merge branch 'as/pre-push-hook'

Add an extra hook so that "git push" that is run without making
sure what is being pushed is sane can be checked and rejected (as
opposed to the user deciding not pushing).

* as/pre-push-hook:
  Add sample pre-push hook script
  push: Add support for pre-push hooks
  hooks: Add function to check if a hook exists
This commit is contained in:
Junio C Hamano 2013-01-23 21:19:25 -08:00
commit bb9a69694f
10 changed files with 302 additions and 20 deletions

View File

@ -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
~~~~~~~~~~~

View File

@ -1329,8 +1329,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)
{
@ -1341,10 +1339,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;

View File

@ -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()
};

View File

@ -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[] = {

View File

@ -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);

View File

@ -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

131
t/t5571-pre-push-hook.sh Executable file
View File

@ -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" <<EOF
cat >/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" <<EOF
cat >/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 <<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

View File

@ -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

View File

@ -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)

View File

@ -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)