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