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 <aaron@schrab.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Aaron Schrab committed Jan 13, 2013 at 00:17 UTC ec55559f937727bcb0fa8a3dfe6af68c188e968a
5 files changed +222
Documentation/githooks.txt
+29
@@ -176,6 +176,35 @@ save and restore any form of metadata associated with the working tree
176 (eg: permissions/ownership, ACLS, etc). See contrib/hooks/setgitperms.perl
177 for an example of how to do this.
178
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 +
208 [[pre-receive]]
209 pre-receive
210 ~~~~~~~~~~~
builtin/push.c
+1
@@ -407,6 +407,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
407 OPT_BOOL(0, "progress", &progress, N_("force progress reporting")),
408 OPT_BIT(0, "prune", &flags, N_("prune locally removed refs"),
409 TRANSPORT_PUSH_PRUNE),
410 + OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
411 OPT_END()
412 };
413
t/t5571-pre-push-hook.sh new
+131
@@ -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
+60
@@ -1034,6 +1034,62 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)
1034 die("Aborting.");
1035 }
1036
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 +
1093 int transport_push(struct transport *transport,
1094 int refspec_nr, const char **refspec, int flags,
1095 unsigned int *reject_reasons)
@@ -1074,6 +1130,10 @@ int transport_push(struct transport *transport,
1130 flags & TRANSPORT_PUSH_MIRROR,
1131 flags & TRANSPORT_PUSH_FORCE);
1132
1133 + if (!(flags & TRANSPORT_PUSH_NO_HOOK))
1134 + if (run_pre_push_hook(transport, remote_refs))
1135 + return -1;
1136 +
1137 if ((flags & TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND) && !is_bare_repository()) {
1138 struct ref *ref = remote_refs;
1139 for (; ref; ref = ref->next)
transport.h
+1
@@ -104,6 +104,7 @@ struct transport {
104 #define TRANSPORT_RECURSE_SUBMODULES_CHECK 64
105 #define TRANSPORT_PUSH_PRUNE 128
106 #define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256
107 +#define TRANSPORT_PUSH_NO_HOOK 512
108
109 #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
110 #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)