1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
|
#!/bin/sh
# ----------------------------------------------------------------------
# post-receive hook to adopt push certs into 'refs/push-certs'
# Collects the cert blob on push and saves it, then, if a certain number of
# signed pushes have been seen, processes all the "saved" blobs in one go,
# adding them to the special ref 'refs/push-certs'. This is done in a way
# that allows searching for all the certs pertaining to one specific branch
# (thanks to Junio Hamano for this idea plus general brainstorming).
# The "collection" happens only if $GIT_PUSH_CERT_NONCE_STATUS = OK; again,
# thanks to Junio for pointing this out; see [1]
#
# [1]: https://groups.google.com/forum/#!topic/gitolite/7cSrU6JorEY
# WARNINGS:
# Does not check that GIT_PUSH_CERT_STATUS = "G". If you want to check that
# and FAIL the push, you'll have to write a simple pre-receive hook
# (post-receive is not the place for that; see 'man githooks').
#
# Gitolite users: failing the hook cannot be done as a VREF because git does
# not set those environment variables in the update hook. You'll have to
# write a trivial pre-receive hook and add that in.
# Relevant gitolite doc links:
# repo-specific environment variables
# http://gitolite.com/gitolite/dev-notes.html#rsev
# repo-specific hooks
# http://gitolite.com/gitolite/non-core.html#rsh
# http://gitolite.com/gitolite/cookbook.html#v3.6-variation-repo-specific-hooks
# Environment:
# GIT_PUSH_CERT_NONCE_STATUS should be "OK" (as mentioned above)
#
# GL_OPTIONS_GPC_PENDING (optional; defaults to 1). This is the number of
# git push certs that should be waiting in order to trigger the post
# processing. You can set it within gitolite like so:
#
# repo foo bar # or maybe just 'repo @all'
# option ENV.GPC_PENDING = 5
# Setup:
# Set up this code as a post-receive hook for whatever repos you need to.
# Then arrange to have the environment variable GL_OPTION_GPC_PENDING set to
# some number, as shown above. (This is only required if you need it to be
# greater than 1.) It could of course be different for different repos.
# Also see "Invocation" section below.
# Invocation:
# Normally via git (see 'man githooks'), once it is setup as a post-receive
# hook.
#
# However, if you set the "pending" limit high, and want to periodically
# "clean up" pending certs without necessarily waiting for the counter to
# trip, do the following (untested):
#
# RB=$(gitolite query-rc GL_REPO_BASE)
# for r in $(gitolite list-phy-repos)
# do
# cd $RB/$repo.git
# unset GL_OPTIONS_GPC_PENDING # if it is set higher up
# hooks/post-receive post_process
# done
#
# That will take care of it.
# Using without gitolite:
# Just set GL_OPTIONS_GPC_PENDING within the script (maybe read it from git
# config). Everything else is independent of gitolite.
# ----------------------------------------------------------------------
# make it work on BSD also (but NOT YET TESTED on FreeBSD!)
uname_s=`uname -s`
if [ "$uname_s" = "Linux" ]
then
_lock() { flock "$@"; }
else
_lock() { lockf -k "$@"; }
# I'm assuming other BSDs also have this; I only have FreeBSD.
fi
# ----------------------------------------------------------------------
# standard stuff
die() { echo "$@" >&2; exit 1; }
warn() { echo "$@" >&2; }
# ----------------------------------------------------------------------
# if there are no arguments, we're running as a "post-receive" hook
if [ -z "$1" ]
then
# ignore if it may be a replay attack
[ "$GIT_PUSH_CERT_NONCE_STATUS" = "OK" ] || exit 1
# I don't think "exit 1" does anything in a post-receive anyway, so that's
# just a symbolic gesture!
# note the lock file used
_lock .gpc.lock $0 cat_blob
# if you want to initiate the post-processing ONLY from outside (for
# example via cron), comment out the next line.
exec $0 post_process
fi
# ----------------------------------------------------------------------
# the 'post_process' part; see "Invocation" section in the doc at the top
if [ "$1" = "post_process" ]
then
# this is the same lock file as above
_lock .gpc.lock $0 count_and_rotate $$
[ -d git-push-certs.$$ ] || exit 0
# but this is a different one
_lock .gpc.ref.lock $0 update_ref $$
exit 0
fi
# ----------------------------------------------------------------------
# other values for "$1" are internal use only
if [ "$1" = "cat_blob" ]
then
mkdir -p git-push-certs
git cat-file blob $GIT_PUSH_CERT > git-push-certs/$GIT_PUSH_CERT
echo $GIT_PUSH_CERT >> git-push-certs/.blob.list
fi
if [ "$1" = "count_and_rotate" ]
then
count=$(ls git-push-certs | wc -l)
if test $count -ge ${GL_OPTIONS_GPC_PENDING:-1}
then
# rotate the directory
mv git-push-certs git-push-certs.$2
fi
fi
if [ "$1" = "update_ref" ]
then
# use a different index file for all this
GIT_INDEX_FILE=push_certs_index; export GIT_INDEX_FILE
# prepare the special ref to receive commits
# historically this hook put the certs in a ref named refs/push-certs
# however, git does *NOT* replicate single-level refs
# trying to push them explicitly causes this error:
# remote: error: refusing to create funny ref 'refs/push-certs' remotely
# https://lore.kernel.org/git/robbat2-20211115T063838-612792475Z@orbis-terrarum.net/
#
# As a good-enough solution, use the namespace of meta/ for the refs.
# This is already used in other systems:
# - kernel.org refs/meta/cgit
# - gerrit refs/meta/config
# - GitBlit reflog: refs/meta/gitblit https://www.gitblit.com/administration.html#H12
# - cc-utils refs/meta/ci
# - JGit refs/meta/push-certs https://www.ibm.com/docs/en/radfws/9.6.1?topic=SSRTLW_9.6.1/org.eclipse.egit.doc/help/JGit/New_and_Noteworthy/4.1/4.1.htm
#
# To migrate from old to new, for each repo:
# git update-ref refs/meta/push-certs refs/push-certs
PUSH_CERTS_EXTRA_REFS='' PUSH_CERTS='' # These vars will be populated after checks.
# others vars are temp
_OLD_PUSH_CERTS=refs/push-certs
_NEW_PUSH_CERTS=refs/meta/push-certs
_OLD_PUSH_CERTS_EXISTS=0
_NEW_PUSH_CERTS_EXISTS=0
git show-ref --verify --quiet -- "$_OLD_PUSH_CERTS" && _OLD_PUSH_CERTS_EXISTS=1
git show-ref --verify --quiet -- "$_NEW_PUSH_CERTS" && _NEW_PUSH_CERTS_EXISTS=1
case "${_OLD_PUSH_CERTS_EXISTS}${_NEW_PUSH_CERTS_EXISTS}" in
# neither or new only:
# let's push to the NEW name only
'00'|'01') PUSH_CERTS=$_NEW_PUSH_CERTS ;;
# old-only: stick to the same, the migration is opt-in
'10') PUSH_CERTS=$_OLD_PUSH_CERTS ;;
# Both: Push to the old name, duplicate to the new name
'11') PUSH_CERTS=$_OLD_PUSH_CERTS PUSH_CERTS_EXTRA_REFS=$_NEW_PUSH_CERTS ;;
esac
# cleanup vars
unset _OLD_PUSH_CERTS_EXISTS _NEW_PUSH_CERTS_EXISTS _OLD_PUSH_CERTS _NEW_PUSH_CERTS
if git rev-parse -q --verify $PUSH_CERTS >/dev/null
then
git read-tree $PUSH_CERTS
else
git read-tree --empty
T=$(git write-tree)
C=$(echo 'start' | git commit-tree $T)
for _ref in $PUSH_CERTS $PUSH_CERTS_EXTRA_REFS ; do
git update-ref "${_ref}" "${C}"
done
fi
# for each cert blob...
for b in `cat git-push-certs.$2/.blob.list`
do
cf=git-push-certs.$2/$b
# it's highly unlikely that the blob got GC-ed already but write it
# back anyway, just in case
B=$(git hash-object -w $cf)
# bit of a sanity check
[ "$B" = "$b" ] || warn "this should not happen: $B is not equal to $b"
# for each ref described within the cert, update the index
for ref in `cat $cf | egrep '^[a-f0-9]+ [a-f0-9]+ refs/' | cut -f3 -d' '`
do
git update-index --add --cacheinfo 100644,$b,$ref
# we're using the ref name as a "fake" filename, so people can,
# for example, 'git log refs/push-certs -- refs/heads/master', to
# see all the push certs pertaining to the master branch. This
# idea came from Junio Hamano, the git maintainer (I certainly
# don't deal with git plumbing enough to have thought of it!)
done
T=$(git write-tree)
C=$( git commit-tree -p $PUSH_CERTS $T < $cf )
for _ref in $PUSH_CERTS $PUSH_CERTS_EXTRA_REFS ; do
git update-ref "${_ref}" "${C}"
done
rm -f $cf
done
rm -f git-push-certs.$2/.blob.list
rmdir git-push-certs.$2
fi
|