Skip to content

Commit ec55559

Browse files
aschrabgitster
authored andcommitted
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: <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> 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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 5a7da2d commit ec55559

File tree

5 files changed

+222
-0
lines changed

5 files changed

+222
-0
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/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

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

transport.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,62 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)
10341034
die("Aborting.");
10351035
}
10361036

1037+
static int run_pre_push_hook(struct transport *transport,
1038+
struct ref *remote_refs)
1039+
{
1040+
int ret = 0, x;
1041+
struct ref *r;
1042+
struct child_process proc;
1043+
struct strbuf buf;
1044+
const char *argv[4];
1045+
1046+
if (!(argv[0] = find_hook("pre-push")))
1047+
return 0;
1048+
1049+
argv[1] = transport->remote->name;
1050+
argv[2] = transport->url;
1051+
argv[3] = NULL;
1052+
1053+
memset(&proc, 0, sizeof(proc));
1054+
proc.argv = argv;
1055+
proc.in = -1;
1056+
1057+
if (start_command(&proc)) {
1058+
finish_command(&proc);
1059+
return -1;
1060+
}
1061+
1062+
strbuf_init(&buf, 256);
1063+
1064+
for (r = remote_refs; r; r = r->next) {
1065+
if (!r->peer_ref) continue;
1066+
if (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;
1067+
if (r->status == REF_STATUS_UPTODATE) continue;
1068+
1069+
strbuf_reset(&buf);
1070+
strbuf_addf( &buf, "%s %s %s %s\n",
1071+
r->peer_ref->name, sha1_to_hex(r->new_sha1),
1072+
r->name, sha1_to_hex(r->old_sha1));
1073+
1074+
if (write_in_full(proc.in, buf.buf, buf.len) != buf.len) {
1075+
ret = -1;
1076+
break;
1077+
}
1078+
}
1079+
1080+
strbuf_release(&buf);
1081+
1082+
x = close(proc.in);
1083+
if (!ret)
1084+
ret = x;
1085+
1086+
x = finish_command(&proc);
1087+
if (!ret)
1088+
ret = x;
1089+
1090+
return ret;
1091+
}
1092+
10371093
int transport_push(struct transport *transport,
10381094
int refspec_nr, const char **refspec, int flags,
10391095
unsigned int *reject_reasons)
@@ -1074,6 +1130,10 @@ int transport_push(struct transport *transport,
10741130
flags & TRANSPORT_PUSH_MIRROR,
10751131
flags & TRANSPORT_PUSH_FORCE);
10761132

1133+
if (!(flags & TRANSPORT_PUSH_NO_HOOK))
1134+
if (run_pre_push_hook(transport, remote_refs))
1135+
return -1;
1136+
10771137
if ((flags & TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND) && !is_bare_repository()) {
10781138
struct ref *ref = remote_refs;
10791139
for (; ref; ref = ref->next)

transport.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ struct transport {
104104
#define TRANSPORT_RECURSE_SUBMODULES_CHECK 64
105105
#define TRANSPORT_PUSH_PRUNE 128
106106
#define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256
107+
#define TRANSPORT_PUSH_NO_HOOK 512
107108

108109
#define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
109110
#define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)

0 commit comments

Comments
 (0)