Skip to content

Commit

Permalink
Merge branch 'as/pre-push-hook'
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gitster committed Jan 24, 2013
2 parents 86db746 + 87c86dd commit bb9a696
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 20 deletions.
29 changes: 29 additions & 0 deletions Documentation/githooks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~
Expand Down
6 changes: 2 additions & 4 deletions builtin/commit.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;

Expand Down
1 change: 1 addition & 0 deletions builtin/push.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()
};

Expand Down
25 changes: 11 additions & 14 deletions builtin/receive-pack.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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[] = {
Expand Down
15 changes: 13 additions & 2 deletions run-command.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions run-command.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions t/t5571-pre-push-hook.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 53 additions & 0 deletions templates/hooks--pre-push.sample
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit bb9a696

Please sign in to comment.