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:
commit
bb9a69694f
@ -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
|
||||
~~~~~~~~~~~
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
};
|
||||
|
||||
|
@ -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[] = {
|
||||
|
@ -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);
|
||||
|
@ -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
131
t/t5571-pre-push-hook.sh
Executable 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
|
53
templates/hooks--pre-push.sample
Normal file
53
templates/hooks--pre-push.sample
Normal 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
|
60
transport.c
60
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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user