Skip to content

Commit bb9a696

Browse files
committed
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
2 parents 86db746 + 87c86dd commit bb9a696

File tree

10 files changed

+302
-20
lines changed

10 files changed

+302
-20
lines changed

Documentation/githooks.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,35 @@ save and restore any form of metadata associated with the working tree
176176
(eg: permissions/ownership, ACLS, etc). See contrib/hooks/setgitperms.perl
177177
for an example of how to do this.
178178

179+
pre-push
180+
~~~~~~~~
181+
182+
This hook is called by 'git push' and can be used to prevent a push from taking
183+
place. The hook is called with two parameters which provide the name and
184+
location of the destination remote, if a named remote is not being used both
185+
values will be the same.
186+
187+
Information about what is to be pushed is provided on the hook's standard
188+
input with lines of the form:
189+
190+
<local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
191+
192+
For instance, if the command +git push origin master:foreign+ were run the
193+
hook would receive a line like the following:
194+
195+
refs/heads/master 67890 refs/heads/foreign 12345
196+
197+
although the full, 40-character SHA1s would be supplied. If the foreign ref
198+
does not yet exist the `<remote SHA1>` will be 40 `0`. If a ref is to be
199+
deleted, the `<local ref>` will be supplied as `(delete)` and the `<local
200+
SHA1>` will be 40 `0`. If the local commit was specified by something other
201+
than a name which could be expanded (such as `HEAD~`, or a SHA1) it will be
202+
supplied as it was originally given.
203+
204+
If this hook exits with a non-zero status, 'git push' will abort without
205+
pushing anything. Information about why the push is rejected may be sent
206+
to the user by writing to standard error.
207+
179208
[[pre-receive]]
180209
pre-receive
181210
~~~~~~~~~~~

builtin/commit.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,8 +1329,6 @@ static int git_commit_config(const char *k, const char *v, void *cb)
13291329
return git_status_config(k, v, s);
13301330
}
13311331

1332-
static const char post_rewrite_hook[] = "hooks/post-rewrite";
1333-
13341332
static int run_rewrite_hook(const unsigned char *oldsha1,
13351333
const unsigned char *newsha1)
13361334
{
@@ -1341,10 +1339,10 @@ static int run_rewrite_hook(const unsigned char *oldsha1,
13411339
int code;
13421340
size_t n;
13431341

1344-
if (access(git_path(post_rewrite_hook), X_OK) < 0)
1342+
argv[0] = find_hook("post-rewrite");
1343+
if (!argv[0])
13451344
return 0;
13461345

1347-
argv[0] = git_path(post_rewrite_hook);
13481346
argv[1] = "amend";
13491347
argv[2] = NULL;
13501348

builtin/push.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
407407
OPT_BOOL(0, "progress", &progress, N_("force progress reporting")),
408408
OPT_BIT(0, "prune", &flags, N_("prune locally removed refs"),
409409
TRANSPORT_PUSH_PRUNE),
410+
OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
410411
OPT_END()
411412
};
412413

builtin/receive-pack.c

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,6 @@ struct command {
182182
char ref_name[FLEX_ARRAY]; /* more */
183183
};
184184

185-
static const char pre_receive_hook[] = "hooks/pre-receive";
186-
static const char post_receive_hook[] = "hooks/post-receive";
187-
188185
static void rp_error(const char *err, ...) __attribute__((format (printf, 1, 2)));
189186
static void rp_warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
190187

@@ -242,10 +239,10 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta
242239
const char *argv[2];
243240
int code;
244241

245-
if (access(hook_name, X_OK) < 0)
242+
argv[0] = find_hook(hook_name);
243+
if (!argv[0])
246244
return 0;
247245

248-
argv[0] = hook_name;
249246
argv[1] = NULL;
250247

251248
memset(&proc, 0, sizeof(proc));
@@ -331,15 +328,14 @@ static int run_receive_hook(struct command *commands, const char *hook_name,
331328

332329
static int run_update_hook(struct command *cmd)
333330
{
334-
static const char update_hook[] = "hooks/update";
335331
const char *argv[5];
336332
struct child_process proc;
337333
int code;
338334

339-
if (access(update_hook, X_OK) < 0)
335+
argv[0] = find_hook("update");
336+
if (!argv[0])
340337
return 0;
341338

342-
argv[0] = update_hook;
343339
argv[1] = cmd->ref_name;
344340
argv[2] = sha1_to_hex(cmd->old_sha1);
345341
argv[3] = sha1_to_hex(cmd->new_sha1);
@@ -532,24 +528,25 @@ static const char *update(struct command *cmd)
532528
}
533529
}
534530

535-
static char update_post_hook[] = "hooks/post-update";
536-
537531
static void run_update_post_hook(struct command *commands)
538532
{
539533
struct command *cmd;
540534
int argc;
541535
const char **argv;
542536
struct child_process proc;
537+
char *hook;
543538

539+
hook = find_hook("post-update");
544540
for (argc = 0, cmd = commands; cmd; cmd = cmd->next) {
545541
if (cmd->error_string || cmd->did_not_exist)
546542
continue;
547543
argc++;
548544
}
549-
if (!argc || access(update_post_hook, X_OK) < 0)
545+
if (!argc || !hook)
550546
return;
547+
551548
argv = xmalloc(sizeof(*argv) * (2 + argc));
552-
argv[0] = update_post_hook;
549+
argv[0] = hook;
553550

554551
for (argc = 1, cmd = commands; cmd; cmd = cmd->next) {
555552
char *p;
@@ -704,7 +701,7 @@ static void execute_commands(struct command *commands, const char *unpacker_erro
704701
0, &cmd))
705702
set_connectivity_errors(commands);
706703

707-
if (run_receive_hook(commands, pre_receive_hook, 0)) {
704+
if (run_receive_hook(commands, "pre-receive", 0)) {
708705
for (cmd = commands; cmd; cmd = cmd->next) {
709706
if (!cmd->error_string)
710707
cmd->error_string = "pre-receive hook declined";
@@ -994,7 +991,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
994991
unlink_or_warn(pack_lockfile);
995992
if (report_status)
996993
report(commands, unpack_status);
997-
run_receive_hook(commands, post_receive_hook, 1);
994+
run_receive_hook(commands, "post-receive", 1);
998995
run_update_post_hook(commands);
999996
if (auto_gc) {
1000997
const char *argv_gc_auto[] = {

run-command.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,15 @@ int finish_async(struct async *async)
735735
#endif
736736
}
737737

738+
char *find_hook(const char *name)
739+
{
740+
char *path = git_path("hooks/%s", name);
741+
if (access(path, X_OK) < 0)
742+
path = NULL;
743+
744+
return path;
745+
}
746+
738747
int run_hook(const char *index_file, const char *name, ...)
739748
{
740749
struct child_process hook;
@@ -744,11 +753,13 @@ int run_hook(const char *index_file, const char *name, ...)
744753
va_list args;
745754
int ret;
746755

747-
if (access(git_path("hooks/%s", name), X_OK) < 0)
756+
p = find_hook(name);
757+
if (!p)
748758
return 0;
749759

760+
argv_array_push(&argv, p);
761+
750762
va_start(args, name);
751-
argv_array_push(&argv, git_path("hooks/%s", name));
752763
while ((p = va_arg(args, const char *)))
753764
argv_array_push(&argv, p);
754765
va_end(args);

run-command.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ int start_command(struct child_process *);
4545
int finish_command(struct child_process *);
4646
int run_command(struct child_process *);
4747

48+
extern char *find_hook(const char *name);
4849
extern int run_hook(const char *index_file, const char *name, ...);
4950

5051
#define RUN_COMMAND_NO_STDIN 1

t/t5571-pre-push-hook.sh

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/bin/sh
2+
3+
test_description='check pre-push hooks'
4+
. ./test-lib.sh
5+
6+
# Setup hook that always succeeds
7+
HOOKDIR="$(git rev-parse --git-dir)/hooks"
8+
HOOK="$HOOKDIR/pre-push"
9+
mkdir -p "$HOOKDIR"
10+
write_script "$HOOK" <<EOF
11+
cat >/dev/null
12+
exit 0
13+
EOF
14+
15+
test_expect_success 'setup' '
16+
git config push.default upstream &&
17+
git init --bare repo1 &&
18+
git remote add parent1 repo1 &&
19+
test_commit one &&
20+
git push parent1 HEAD:foreign
21+
'
22+
write_script "$HOOK" <<EOF
23+
cat >/dev/null
24+
exit 1
25+
EOF
26+
27+
COMMIT1="$(git rev-parse HEAD)"
28+
export COMMIT1
29+
30+
test_expect_success 'push with failing hook' '
31+
test_commit two &&
32+
test_must_fail git push parent1 HEAD
33+
'
34+
35+
test_expect_success '--no-verify bypasses hook' '
36+
git push --no-verify parent1 HEAD
37+
'
38+
39+
COMMIT2="$(git rev-parse HEAD)"
40+
export COMMIT2
41+
42+
write_script "$HOOK" <<'EOF'
43+
echo "$1" >actual
44+
echo "$2" >>actual
45+
cat >>actual
46+
EOF
47+
48+
cat >expected <<EOF
49+
parent1
50+
repo1
51+
refs/heads/master $COMMIT2 refs/heads/foreign $COMMIT1
52+
EOF
53+
54+
test_expect_success 'push with hook' '
55+
git push parent1 master:foreign &&
56+
diff expected actual
57+
'
58+
59+
test_expect_success 'add a branch' '
60+
git checkout -b other parent1/foreign &&
61+
test_commit three
62+
'
63+
64+
COMMIT3="$(git rev-parse HEAD)"
65+
export COMMIT3
66+
67+
cat >expected <<EOF
68+
parent1
69+
repo1
70+
refs/heads/other $COMMIT3 refs/heads/foreign $COMMIT2
71+
EOF
72+
73+
test_expect_success 'push to default' '
74+
git push &&
75+
diff expected actual
76+
'
77+
78+
cat >expected <<EOF
79+
parent1
80+
repo1
81+
refs/tags/one $COMMIT1 refs/tags/tag1 $_z40
82+
HEAD~ $COMMIT2 refs/heads/prev $_z40
83+
EOF
84+
85+
test_expect_success 'push non-branches' '
86+
git push parent1 one:tag1 HEAD~:refs/heads/prev &&
87+
diff expected actual
88+
'
89+
90+
cat >expected <<EOF
91+
parent1
92+
repo1
93+
(delete) $_z40 refs/heads/prev $COMMIT2
94+
EOF
95+
96+
test_expect_success 'push delete' '
97+
git push parent1 :prev &&
98+
diff expected actual
99+
'
100+
101+
cat >expected <<EOF
102+
repo1
103+
repo1
104+
HEAD $COMMIT3 refs/heads/other $_z40
105+
EOF
106+
107+
test_expect_success 'push to URL' '
108+
git push repo1 HEAD &&
109+
diff expected actual
110+
'
111+
112+
# Test that filling pipe buffers doesn't cause failure
113+
# Too slow to leave enabled for general use
114+
if false
115+
then
116+
printf 'parent1\nrepo1\n' >expected
117+
nr=1000
118+
while test $nr -lt 2000
119+
do
120+
nr=$(( $nr + 1 ))
121+
git branch b/$nr $COMMIT3
122+
echo "refs/heads/b/$nr $COMMIT3 refs/heads/b/$nr $_z40" >>expected
123+
done
124+
125+
test_expect_success 'push many refs' '
126+
git push parent1 "refs/heads/b/*:refs/heads/b/*" &&
127+
diff expected actual
128+
'
129+
fi
130+
131+
test_done

templates/hooks--pre-push.sample

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/sh
2+
3+
# An example hook script to verify what is about to be pushed. Called by "git
4+
# push" after it has checked the remote status, but before anything has been
5+
# pushed. If this script exits with a non-zero status nothing will be pushed.
6+
#
7+
# This hook is called with the following parameters:
8+
#
9+
# $1 -- Name of the remote to which the push is being done
10+
# $2 -- URL to which the push is being done
11+
#
12+
# If pushing without using a named remote those arguments will be equal.
13+
#
14+
# Information about the commits which are being pushed is supplied as lines to
15+
# the standard input in the form:
16+
#
17+
# <local ref> <local sha1> <remote ref> <remote sha1>
18+
#
19+
# This sample shows how to prevent push of commits where the log message starts
20+
# with "WIP" (work in progress).
21+
22+
remote="$1"
23+
url="$2"
24+
25+
z40=0000000000000000000000000000000000000000
26+
27+
IFS=' '
28+
while read local_ref local_sha remote_ref remote_sha
29+
do
30+
if [ "$local_sha" = $z40 ]
31+
then
32+
# Handle delete
33+
else
34+
if [ "$remote_sha" = $z40 ]
35+
then
36+
# New branch, examine all commits
37+
range="$local_sha"
38+
else
39+
# Update to existing branch, examine new commits
40+
range="$remote_sha..$local_sha"
41+
fi
42+
43+
# Check for WIP commit
44+
commit=`git rev-list -n 1 --grep '^WIP' "$range"`
45+
if [ -n "$commit" ]
46+
then
47+
echo "Found WIP commit in $local_ref, not pushing"
48+
exit 1
49+
fi
50+
fi
51+
done
52+
53+
exit 0

0 commit comments

Comments
 (0)