taskd/clone.sh: persistently attempt git-svn clones
[girocco.git] / shlib.sh
blobb65bb1ee5156b8f08dd6e388a12c10b310a981ff
1 #!/bin/sh
3 # This is generic shell library for all the scripts used by Girocco;
4 # most importantly, it introduces all the $cfg_* shell variables.
6 # hash patterns
7 hexdig='[0-9a-f]'
8 octet="$hexdig$hexdig"
9 octet4="$octet$octet$octet$octet"
10 octet19="$octet4$octet4$octet4$octet4$octet$octet$octet"
11 octet20="$octet4$octet4$octet4$octet4$octet4"
12 # tab (single \t between single quotes)
13 tab=' '
15 # set a sane umask that never excludes any user or group permissions
16 umask $(printf '0%03o' $(( $(umask) & ~0770 )) )
18 vcmp() {
19 # Compare $1 to $2 each of which must match \d+(\.\d+)*
20 # An empty string ('') for $1 or $2 is treated like 0
21 # Outputs:
22 # -1 if $1 < $2
23 # 0 if $1 = $2
24 # 1 if $1 > $2
25 # Note that `vcmp 1.8 1.8.0.0.0.0` correctly outputs 0.
26 while
27 _a="${1%%.*}"
28 _b="${2%%.*}"
29 [ -n "$_a" ] || [ -n "$_b" ]
31 if [ "${_a:-0}" -lt "${_b:-0}" ]; then
32 echo -1
33 return
34 elif [ "${_a:-0}" -gt "${_b:-0}" ]; then
35 echo 1
36 return
38 _a2="${1#$_a}"
39 _b2="${2#$_b}"
40 set -- "${_a2#.}" "${_b2#.}"
41 done
42 echo 0
45 unset orig_path
46 get_girocco_config_pm_var_list() (
47 # Export all the variables from Girocco::Config to suitable var= lines
48 # prefixing them with 'cfg_'. E.g. $cfg_admin is admin's mail address now
49 # and also setting a 'defined_cfg_' prefix to 1 if they are not undef.
50 __girocco_conf="$GIROCCO_CONF"
51 [ -n "$__girocco_conf" ] || __girocco_conf="Girocco::Config"
52 [ -z "$basedir" ] || __girocco_extrainc="-I$basedir"
53 inc_basedir=@basedir@
54 [ "@basedir@" != '@'basedir'@' ] || inc_basedir="$PWD"
55 [ -z "$orig_path" ] || { PATH="$orig_path" && export PATH; }
56 perl -I"$inc_basedir" $__girocco_extrainc -M$__girocco_conf -le \
57 'foreach (sort {uc($a) cmp uc($b)} keys %Girocco::Config::) {
58 my $val = ${$Girocco::Config::{$_}}; defined($val) or $val="";
59 $val =~ s/([\\"\$\`])/\\$1/gos;
60 $val =~ s/(?:\r\n|\r|\n)$//os;
61 print "cfg_$_=\"$val\"";
62 print "defined_cfg_$_=",
63 (defined(${$Girocco::Config::{$_}})?"1":"");
67 # Returns full command path for "$1" if it's a valid command otherwise returns "$1"
68 _fcp() {
69 if _fp="$(command -v "$1" 2>/dev/null)"; then
70 printf '%s\n' "$_fp"
71 else
72 printf '%s\n' "$1"
76 get_girocco_config_var_list() (
77 # Same as get_girocco_config_pm_var_list except that
78 # the following variables (all starting with var_) are added:
79 # var_group cfg_owning_group if defined otherwise `id -gn`
80 # var_group_gid group id number of $var_group
81 # var_mirror_uid user id number of $cfg_mirror_user
82 # var_cgi_uid user id number of $cfg_cgi_user
83 # var_git_ver The version number part from `git version`
84 # var_git_exec_path The result of $cfg_git_bin --exec-dir
85 # var_sh_bin Full path to the posix sh interpreter to use
86 # var_perl_bin Full path to the perl interpreter to use
87 # var_gzip_bin Full path to the gzip executable to use
88 # var_nc_openbsd_bin Full path to the netcat (nc) with -U support
89 # var_have_git_171 Set to 1 if git version >= 1.7.1 otherwise ''
90 # var_have_git_172 Set to 1 if git version >= 1.7.2 otherwise ''
91 # var_have_git_173 Set to 1 if git version >= 1.7.3 otherwise ''
92 # var_have_git_1710 Set to 1 if git version >= 1.7.10 otherwise ''
93 # var_have_git_185 Set to 1 if git version >= 1.8.5 otherwise ''
94 # var_have_git_210 Set to 1 if git version >= 2.1.0 otherwise ''
95 # var_have_git_235 Set to 1 if git version >= 2.3.5 otherwise ''
96 # var_have_git_260 Set to 1 if git version >= 2.6.0 otherwise ''
97 # var_have_git_2101 Set to 1 if git version >= 2.10.1 otherwise ''
98 # var_window_memory Value to use for repack --window-memory=
99 # var_big_file_threshold Value to use for core.bigFileThreshold
100 # var_redelta_threshold Recompute deltas if no more than this many objs
101 # var_upload_window If not "", pack.window to use for upload-pack
102 # var_log_window_size Value to use for git-svn --log-window-size=
103 # var_utf8_locale Value to use for a UTF-8 locale if available
104 # var_xargs_r A "-r" if xargs needs it to behave correctly
105 # var_du_exclude Option to exclude PATTERN from du if available
106 # var_du_follow Option to follow command line sym links if available
107 _cfg_vars="$(get_girocco_config_pm_var_list)"
108 eval "$_cfg_vars"
109 printf '%s\n' "$_cfg_vars"
110 printf 'var_group=%s\n' "${cfg_owning_group:-$(id -gn)}"
111 perl - "$var_group" "$cfg_mirror_user" "$cfg_cgi_user" <<-'PERLPROG'
112 no warnings;
113 my $gid = getgrnam($ARGV[0]);
114 my $mid = getpwnam($ARGV[1]);
115 my $cid = getpwnam($ARGV[2]);
116 defined($gid) && $gid ne '' and print "var_group_gid=$gid\n";
117 defined($mid) && $mid ne '' and print "var_mirror_uid=$mid\n";
118 defined($cid) && $cid ne '' and print "var_cgi_uid=$cid\n";
119 PERLPROG
120 _gver="$("$cfg_git_bin" version 2>/dev/null |
121 LC_ALL=C sed -ne 's/^[^0-9]*\([0-9][0-9]*\(\.[0-9][0-9]*\)*\).*$/\1/p')"
122 printf 'var_git_ver=%s\n' "$_gver"
123 printf 'var_git_exec_path="%s"\n' "$("$cfg_git_bin" --exec-path 2>/dev/null)"
124 printf 'var_sh_bin="%s"\n' "$(_fcp "${cfg_posix_sh_bin:-/bin/sh}")"
125 printf 'var_perl_bin="%s"\n' "$(_fcp "${cfg_perl_bin:-$(unset -f perl; command -v perl)}")"
126 printf 'var_gzip_bin="%s"\n' "$(_fcp "${cfg_gzip_bin:-$(unset -f gzip; command -v gzip)}")"
127 printf 'var_nc_openbsd_bin="%s"\n' "$(_fcp "${cfg_nc_openbsd_bin:-$(unset -f nc; command -v nc)}")"
128 printf 'var_have_git_171=%s\n' "$([ $(vcmp "$_gver" 1.7.1) -ge 0 ] && echo 1)"
129 printf 'var_have_git_172=%s\n' "$([ $(vcmp "$_gver" 1.7.2) -ge 0 ] && echo 1)"
130 printf 'var_have_git_173=%s\n' "$([ $(vcmp "$_gver" 1.7.3) -ge 0 ] && echo 1)"
131 printf 'var_have_git_1710=%s\n' "$([ $(vcmp "$_gver" 1.7.10) -ge 0 ] && echo 1)"
132 printf 'var_have_git_185=%s\n' "$([ $(vcmp "$_gver" 1.8.5) -ge 0 ] && echo 1)"
133 printf 'var_have_git_210=%s\n' "$([ $(vcmp "$_gver" 2.1.0) -ge 0 ] && echo 1)"
134 printf 'var_have_git_235=%s\n' "$([ $(vcmp "$_gver" 2.3.5) -ge 0 ] && echo 1)"
135 printf 'var_have_git_260=%s\n' "$([ $(vcmp "$_gver" 2.6.0) -ge 0 ] && echo 1)"
136 printf 'var_have_git_2101=%s\n' "$([ $(vcmp "$_gver" 2.10.1) -ge 0 ] && echo 1)"
137 __girocco_conf="$GIROCCO_CONF"
138 [ -n "$__girocco_conf" ] || __girocco_conf="Girocco::Config"
139 [ -z "$basedir" ] || __girocco_extrainc="-I$basedir"
140 inc_basedir=@basedir@
141 [ "@basedir@" != '@'basedir'@' ] || inc_basedir="$PWD"
142 printf "var_window_memory=%s\n" \
143 "$(perl -I"$inc_basedir" $__girocco_extrainc -M$__girocco_conf \
144 -MGirocco::Util -e 'print calc_windowmemory')"
145 printf "var_big_file_threshold=%s\n" \
146 "$(perl -I"$inc_basedir" $__girocco_extrainc -M$__girocco_conf \
147 -MGirocco::Util -e 'print calc_bigfilethreshold')"
148 printf "var_redelta_threshold=%s\n" \
149 "$(perl -I"$inc_basedir" $__girocco_extrainc -M$__girocco_conf \
150 -MGirocco::Util -e 'print calc_redeltathreshold')"
151 if [ -n "$cfg_upload_pack_window" ] && [ "$cfg_upload_pack_window" -ge 2 ] &&
152 [ "$cfg_upload_pack_window" -le 50 ]; then
153 printf "var_upload_window=%s\n" "$cfg_upload_pack_window"
154 else
155 printf "var_upload_window=%s\n" ""
157 printf 'var_log_window_size=%s\n' "${cfg_svn_log_window_size:-250}"
158 # We parse the output of `locale -a` and select a suitable UTF-8 locale.
159 _guess_locale="$(locale -a | LC_ALL=C grep -viE '^(posix|c)(\..*)?$' |
160 LC_ALL=C grep -iE '\.utf-?8$' | LC_ALL=C sed -e 's/\.[Uu][Tt][Ff]-*8$//' |
161 LC_ALL=C sed -e '/en_US/ s/^/0 /; /en_US/ !s/^/1 /' | LC_ALL=C sort |
162 head -n 1 | LC_ALL=C cut -d ' ' -f 2)"
163 [ -z "$_guess_locale" ] || printf 'var_utf8_locale=%s.UTF-8\n' "$_guess_locale"
164 # On some broken platforms running xargs without -r and empty input runs the command
165 printf 'var_xargs_r=%s\n' "$(</dev/null command xargs printf %s -r)"
166 # The disk usage report produces better numbers if du has an exclude option
167 _x0="${0##*/}"
168 _x0="${_x0%?}?*"
169 for _duopt in --exclude -I; do
170 if _test="$(du $_duopt 's?lib.s*' $_duopt "$_x0" "$0" 2>/dev/null)" && [ -z "$_test" ]; then
171 printf 'var_du_exclude=%s\n' "$_duopt"
172 break
174 done
175 if _test="$(du -H "$0" 2>/dev/null)" && [ -n "$_test" ]; then
176 printf 'var_du_follow=%s\n' "-H"
177 break
181 # If basedir has been replaced, and shlib_vars.sh exists, get the config
182 # definitions from it rather than running Perl.
183 if [ "@basedir@" = '@'basedir'@' ] || ! [ -r "@basedir@/shlib_vars.sh" ]; then
184 # Import all the variables from Girocco::Config to the local environment,
185 eval "$(get_girocco_config_var_list)"
186 else
187 # Import the variables from shlib_vars.sh which avoids needlessly
188 # running another copy of Perl
189 . "@basedir@/shlib_vars.sh"
192 # git_add_config "some.var=value"
193 # every ' in value must be replaced with the 4-character sequence '\'' before
194 # calling this function or Git will barf. Will not be effective unless running
195 # Git version 1.7.3 or later.
196 git_add_config() {
197 GIT_CONFIG_PARAMETERS="${GIT_CONFIG_PARAMETERS:+$GIT_CONFIG_PARAMETERS }'$1'"
198 export GIT_CONFIG_PARAMETERS
201 # file of empty lines
202 mtlinesfile="$cfg_basedir/mtlinesfile"
203 # created by installer, but if not exists, set to /dev/null
204 [ -e "$mtlinesfile" ] && [ -f "$mtlinesfile" ] && [ -r "$mtlinesfile" ] ||
205 mtlinesfile='/dev/null'
207 # Make sure we have a reproducible environment by using a controlled HOME dir
208 XDG_CONFIG_HOME="$cfg_chroot/var/empty"
209 HOME="$cfg_chroot/etc/girocco"
210 TMPDIR="/tmp"
211 GIT_CONFIG_NOSYSTEM=1
212 GIT_ATTR_NOSYSTEM=1
213 GIT_NO_REPLACE_OBJECTS=1
214 GIT_TERMINAL_PROMPT=0
215 GIT_PAGER="cat"
216 PAGER="cat"
217 GIT_ASKPASS="$cfg_basedir/bin/git-askpass-password"
218 GIT_SVN_NOTTY=1
219 GIROCCO_SUPPRESS_AUTO_GC_UPDATE=1
220 GIT_SSH="$cfg_basedir/bin/git-ssh"
221 SVN_SSH="$cfg_basedir/bin/git-ssh"
222 export XDG_CONFIG_HOME
223 export HOME
224 export TMPDIR
225 export GIT_CONFIG_NOSYSTEM
226 export GIT_ATTR_NOSYSTEM
227 export GIT_NO_REPLACE_OBJECTS
228 export GIT_TERMINAL_PROMPT
229 export GIT_PAGER
230 export PAGER
231 export GIT_ASKPASS
232 export GIT_SVN_NOTTY
233 export GIROCCO_SUPPRESS_AUTO_GC_UPDATE
234 export GIT_SSH
235 export SVN_SSH
236 unset GIT_USER_AGENT
237 unset GIT_HTTP_USER_AGENT
238 if [ -n "$defined_cfg_git_client_ua" ]; then
239 GIT_USER_AGENT="$cfg_git_client_ua"
240 export GIT_USER_AGENT
242 unset GIT_CONFIG_PARAMETERS
245 ## IMPORTANT!
247 ## Keep gitweb/gitweb_config.perl in sync with these git_add_config calls
248 ## Keep bin/git-shell-verify in sync with these git_add_config calls
250 git_add_config "core.ignoreCase=false"
251 git_add_config "core.pager=cat"
252 if [ -n "$cfg_git_no_mmap" ]; then
253 # Just like compiling with NO_MMAP
254 git_add_config "core.packedGitWindowSize=1m"
255 else
256 # Always use the 32-bit default (32m) even on 64-bit to avoid memory blowout
257 git_add_config "core.packedGitWindowSize=32m"
259 # Always use the 32-bit default (256m) even on 64-bit to avoid memory blowout
260 git_add_config "core.packedGitLimit=256m"
261 [ -z "$var_big_file_threshold" ] ||
262 git_add_config "core.bigFileThreshold=$var_big_file_threshold"
263 git_add_config "gc.auto=0"
265 # Make sure any sendmail.pl config is always available
266 unset SENDMAIL_PL_HOST
267 unset SENDMAIL_PL_PORT
268 unset SENDMAIL_PL_NCBIN
269 unset SENDMAIL_PL_NCOPT
270 [ -z "$cfg_sendmail_pl_host" ] || { SENDMAIL_PL_HOST="$cfg_sendmail_pl_host" && export SENDMAIL_PL_HOST; }
271 [ -z "$cfg_sendmail_pl_port" ] || { SENDMAIL_PL_PORT="$cfg_sendmail_pl_port" && export SENDMAIL_PL_PORT; }
272 [ -z "$cfg_sendmail_pl_ncbin" ] || { SENDMAIL_PL_NCBIN="$cfg_sendmail_pl_ncbin" && export SENDMAIL_PL_NCBIN; }
273 [ -z "$cfg_sendmail_pl_ncopt" ] || { SENDMAIL_PL_NCOPT="$cfg_sendmail_pl_ncopt" && export SENDMAIL_PL_NCOPT; }
275 # Set PATH and PYTHON to the values set by Config.pm, if any
276 unset PYTHON
277 [ -z "$cfg_python" ] || { PYTHON="$cfg_python" && export PYTHON; }
278 [ -z "$cfg_path" ] || { orig_path="$PATH" && PATH="$cfg_path" && export PATH; }
280 # Extra GIT variables that generally ought to be cleared, but whose clearing
281 # could potentially interfere with the correct operation of hook scripts so
282 # they are segregated into a separate function for use as appropriate
283 clean_git_env() {
284 unset GIT_ALTERNATE_OBJECT_DIRECTORIES
285 unset GIT_CONFIG
286 unset GIT_DIR
287 unset GIT_GRAFT_FILE
288 unset GIT_INDEX_FILE
289 unset GIT_OBJECT_DIRECTORY
290 unset GIT_NAMESPACE
293 # We cannot use a git() {} or nc_openbsd() {} function to redirect git
294 # and nc_openbsd to the desired executables because when using
295 # "ENV_VAR=xxx func" the various /bin/sh implementations behave in various
296 # different and unexpected ways:
297 # a) treat "ENV_VAR=xxx" like a separate, preceding "export ENV_VAR=xxx"
298 # b) treat "ENV_VAR=xxx" like a separate, prededing "ENV_VAR=xxx"
299 # c) treat "ENV_VAR=xxx" like a temporary setting only while running func
300 # None of these are good. We want a temporary "export ENV_VAR=xxx"
301 # setting only while running func which none of the /bin/sh's do.
303 # Instead we'd like to use an alias that provides the desired behavior without
304 # any of the bad (a), (b) or (c) effects.
306 # However, unfortunately, some of the crazy /bin/sh implementations do not
307 # recognize alias expansions when preceded by variable assignments!
309 # So we are left with git() {} and nc_openbsd() {} functions and in the
310 # case of git() {} we can compensate for (b) and (c) failing to export
311 # but not (a) and (b) persisting the values so the caller will simply
312 # have to beware and explicitly unset any variables that should not persist
313 # beyond the function call itself.
315 git() (
316 [ z"${GIT_DIR+set}" != z"set" ] || export GIT_DIR
317 [ z"${GIT_SSL_NO_VERIFY+set}" != z"set" ] || export GIT_SSL_NO_VERIFY
318 [ z"${GIT_TRACE_PACKET+set}" != z"set" ] || export GIT_TRACE_PACKET
319 [ z"${GIT_USER_AGENT+set}" != z"set" ] || export GIT_USER_AGENT
320 [ z"${GIT_HTTP_USER_AGENT+set}" != z"set" ] || export GIT_HTTP_USER_AGENT
321 exec "$cfg_git_bin" "$@"
324 # Since we do not yet require at least Git 1.8.5 this is a compatibility function
325 # that allows us to use git update-ref --stdin where supported and the slow shell
326 # script where not, but only the "delete" operation is currently supported.
327 git_updateref_stdin() {
328 if [ -n "$var_have_git_185" ]; then
329 git update-ref --stdin
330 else
331 while read -r _op _ref; do
332 case "$_op" in
333 delete)
334 git update-ref -d "$_ref"
337 echo "bad git_updateref_stdin op: $_op" >&2
338 exit 1
340 esac
341 done
345 # see comments for git() -- callers must explicitly export all variables
346 # intended for the commands these functions run before calling them
347 perl() { command "${var_perl_bin:-perl}" "$@"; }
348 gzip() { command "${var_gzip_bin:-gzip}" "$@"; }
350 nc_openbsd() { command "$var_nc_openbsd_bin" "$@"; }
352 list_packs() { command "$cfg_basedir/bin/list_packs" "$@"; }
354 readlink() { command "$cfg_basedir/bin/readlink" "$@"; }
356 strftime() { command "$cfg_basedir/bin/strftime" "$@"; }
358 # Some platforms' broken xargs runs the command always at least once even if
359 # there's no input unless given a special option. Automatically supply the
360 # option on those platforms by providing an xargs function.
361 xargs() { command xargs $var_xargs_r "$@"; }
363 _addrlist() {
364 _list=
365 for _addr in "$@"; do
366 [ -z "$_list" ] || _list="$_list, "
367 _list="$_list$_addr"
368 done
369 echo "$_list"
372 _sendmail() {
373 _mailer="${cfg_sendmail_bin:-/usr/sbin/sendmail}"
374 if [ -n "$cfg_sender" ]; then
375 "$_mailer" -i -f "$cfg_sender" "$@"
376 else
377 "$_mailer" -i "$@"
381 # First argument is an id WITHOUT surrounding '<' and '>' to use in a
382 # "References:" header. It may be "" to suppress the "References" header.
383 # Following arguments are just like mail function
384 mailref() {
385 _references=
386 if [ $# -ge 1 ]; then
387 _references="$1"
388 shift
390 _subject=
391 if [ "$1" = "-s" ]; then
392 shift
393 _subject="$1"
394 shift
397 echo "From: \"$cfg_name\" ($cfg_title) <$cfg_admin>"
398 echo "To: $(_addrlist "$@")"
399 [ -z "$_subject" ] || echo "Subject: $_subject"
400 echo "MIME-Version: 1.0"
401 echo "Content-Type: text/plain; charset=utf-8; format=fixed"
402 echo "Content-Transfer-Encoding: 8bit"
403 [ -z "$_references" ] || echo "References: <$_references>"
404 [ -n "$cfg_suppress_x_girocco" ] || echo "X-Girocco: $cfg_gitweburl"
405 echo "Auto-Submitted: auto-generated"
406 echo ""
408 } | _sendmail "$@"
411 # Usage: mail [-s <subject>] <addr> [<addr>...]
412 mail() {
413 mailref "" "$@"
416 # bang CMD... will execute the command with well-defined failure mode;
417 # set bang_action to string of the failed action ('clone', 'update', ...);
418 # re-define the bang_trap() function to do custom cleanup before bailing out
419 bang() {
420 bang_errcode=
421 bang_catch "$@"
422 [ "${bang_errcode:-0}" = "0" ] || bang_failed
425 bang_catch() {
426 bang_active=1
427 bang_cmd="$*"
428 bang_errcode=0
429 if [ "${show_progress:-0}" != "0" ]; then
430 exec 3>&1
431 read -r bang_errcode <<-EOT || :
433 exec 4>&3 3>&1 1>&4 4>&-
434 { "$@" 3>&- || echo $? >&3; } 2>&1 | tee -i -a "$bang_log"
437 exec 3>&-
438 if [ -z "$bang_errcode" ] || [ "$bang_errcode" = "0" ]; then
439 # All right. Cool.
440 bang_active=
441 bang_cmd=
442 return;
444 else
445 if "$@" >>"$bang_log" 2>&1; then
446 # All right. Cool.
447 bang_active=
448 bang_cmd=
449 return;
450 else
451 bang_errcode="$?"
456 bang_failed() {
457 bang_active=
458 unset GIT_DIR
459 >.banged
460 cat "$bang_log" >.banglog
461 echo "" >>.banglog
462 echo "$bang_cmd failed with error code $bang_errcode" >>.banglog
463 ! [ -d htmlcache ] || { >htmlcache/changed; } 2>/dev/null || :
464 if [ "${show_progress:-0}" != "0" ]; then
465 echo ""
466 echo "$bang_cmd failed with error code $bang_errcode"
468 if [ -e .bangagain ]; then
469 git config --remove-section girocco.bang 2>/dev/null || :
470 rm -f .bangagain
472 bangcount="$(git config --int girocco.bang.count 2>/dev/null)" || :
473 bangcount=$(( ${bangcount:-0} + 1 ))
474 git config --int girocco.bang.count $bangcount
475 if [ $bangcount -eq 1 ]; then
476 git config girocco.bang.firstfail "$(TZ=UTC date "+%Y-%m-%d %T UTC")"
478 if [ $bangcount -ge $cfg_min_mirror_failure_message_count ] &&
479 [ "$(git config --bool girocco.bang.messagesent 2>/dev/null || :)" != "true" ] &&
480 ! check_interval "girocco.bang.firstfail" $cfg_min_mirror_failure_message_interval; then
481 bangmailok="$(git config --bool gitweb.statusupdates 2>/dev/null || echo true)"
482 bangaddrs=
483 [ "$bangmailok" = "false" ] || [ -z "$mail" ] || bangaddrs="$mail"
484 [ -z "$cfg_admincc" ] || [ "$cfg_admincc" = "0" ] || [ -z "$cfg_admin" ] ||
485 if [ -z "$bangaddrs" ]; then bangaddrs="$cfg_admin"; else bangaddrs="$bangaddrs,$cfg_admin"; fi
486 rsubj=
487 [ $bangcount -le 1 ] || rsubj=" repeatedly"
488 [ -z "$bangaddrs" ] ||
490 echo "$bang_cmd failed with error code $bang_errcode"
491 echo ""
492 rsubj=
493 if [ $bangcount -gt 1 ]; then
494 echo "$bangcount consecutive update failures have occurred since $(config_get girocco.bang.firstfail)"
495 echo ""
497 echo "you will not receive any more notifications until recovery"
498 echo "this status message may be disabled on the project admin page"
499 echo ""
500 echo "Log follows:"
501 echo ""
502 cat "$bang_log"
503 } | mailref "update@$cfg_gitweburl/$proj.git" -s "[$cfg_name] $proj $bang_action failed$rsubj" "$bangaddrs"
504 git config --bool girocco.bang.messagesent true
506 bangthrottle=
507 [ $bangcount -lt 15 ] ||
508 check_interval "girocco.bang.firstfail" $(( $cfg_min_mirror_interval * 3 / 2 )) ||
509 bangthrottle=1
510 bang_trap $bangthrottle
511 [ -n "$bang_errcode" ] && [ "$bang_errcode" != "0" ] || bang_errcode=1
512 exit $bang_errcode
515 # bang_eval CMD... will evaluate the command with well-defined failure mode;
516 # Identical to bang CMD... except the command is eval'd instead of executed.
517 bang_eval() {
518 bang eval "$*"
521 # Default bang settings:
522 bang_setup() {
523 bang_active=
524 bang_action="lame_programmer"
525 bang_trap() { :; }
526 bang_tmpdir="${TMPDIR:-/tmp}"
527 bang_tmpdir="${bang_tmpdir%/}"
528 bang_log="$(mktemp "${bang_tmpdir:-/tmp}/repomgr-XXXXXX")"
529 is_git_dir . || {
530 echo "bang_setup called with current directory not a git directory" >&2
531 exit 1
533 trap 'rm -f "$bang_log"' EXIT
534 trap '[ -z "$bang_active" ] || { bang_errcode=130; bang_failed; }; exit 130' INT
535 trap '[ -z "$bang_active" ] || { bang_errcode=143; bang_failed; }; exit 143' TERM
538 # Remove banged status
539 bang_reset() {
540 rm -f .banged .bangagain .banglog
541 git config --remove-section girocco.bang 2>/dev/null || :
544 # Check to see if banged status
545 is_banged() {
546 [ -e .banged ]
549 # Check to see if banged message was sent
550 was_banged_message_sent() {
551 [ "$(git config --bool girocco.bang.messagesent 2>/dev/null || :)" = "true" ]
554 # Progress report - if show_progress is set, shows the given message.
555 progress() {
556 [ "${show_progress:-0}" = "0" ] || echo "$*"
559 # Project config accessors; must be run in project directory
560 config_get() {
561 case "$1" in
562 *.*)
563 git config "$1";;
565 git config "gitweb.$1";;
566 esac
569 config_set() {
570 git config "gitweb.$1" "$2" && chgrp $var_group config && chmod g+w config
573 config_set_raw() {
574 git config "$1" "$2" && chgrp $var_group config && chmod g+w config
577 config_get_date_seconds() {
578 _dt="$(config_get "$1")" || :
579 [ -n "$_dt" ] || return 1
580 _ds="$(perl -I@basedir@ -MGirocco::Util -e "print parse_any_date('$_dt')")"
581 [ -n "$_ds" ] || return 1
582 echo "$_ds"
585 # Tool for checking whether given number of seconds has not passed yet
586 check_interval() {
587 os="$(config_get_date_seconds "$1")" || return 1
588 ns="$(date +%s)"
589 [ $ns -lt $(($os+$2)) ]
592 # Check if we are running with effective root permissions
593 is_root() {
594 [ "$(id -u 2>/dev/null)" = "0" ]
597 # Check to see if the single argument (default ".") is a Git directory
598 is_git_dir() {
599 # Just like Git's test except we ignore GIT_OBJECT_DIRECTORY
600 # And we are slightly more picky (must be refs/.+ not refs/.*)
601 [ $# -ne 0 ] || set -- "."
602 [ -d "$1/objects" ] && [ -x "$1/objects" ] || return 1
603 [ -d "$1/refs" ] && [ -x "$1/refs" ] || return 1
604 if [ -L "$1/HEAD" ]; then
605 _hr="$(readlink "$1/HEAD")"
606 case "$_hr" in "refs/"?*) :;; *) return 1;; esac
608 [ -f "$1/HEAD" ] && [ -r "$1/HEAD" ] || return 1
609 read -r _hr <"$1/HEAD" || return 1
610 case "$_hr" in
611 $octet20*)
612 [ "${_hr#*[!0-9a-f]}" = "$_hr" ] || return 1
613 return 0;;
614 ref:refs/?*)
615 return 0;;
616 ref:*)
617 _hr="${_hr##ref:*[ $tab]}"
618 case "$_hr" in "refs/"?*) return 0;; esac
619 esac
620 return 1
623 # Check to see if the single argument (default ".") is a directory with no refs
624 is_empty_refs_dir() {
625 [ $# -ne 0 ] || set -- "."
626 if [ -s "$1/packed-refs" ]; then
627 # could be a packed-refs file with just a '# pack-refs ..." line
628 # null hash lines and peel lines do not count either
629 _refcnt="$(( $(LC_ALL=C sed <"$1/packed-refs" \
630 -e "/^00* /d" \
631 -e "/^$octet20$hexdig* refs\/[^ $tab]*\$/!d" | wc -l) ))"
632 [ "${_refcnt:-0}" -eq 0 ] || return 1
634 if [ -d "$1/refs" ]; then
635 # quick and dirty check, doesn't try to validate contents
636 # or ignore embedded symbolic refs
637 _refcnt="$(( $(find -L "$1/refs" -type f -print 2>/dev/null | head -n 1 | LC_ALL=C wc -l) ))"
638 [ "${_refcnt:-0}" -eq 0 ] || return 1
640 # last chance a detached HEAD (we ignore any linked working trees though)
641 [ -s "$1/HEAD" ] && read -r _hr <"$1/HEAD" && [ -n "$_hr" ] || return 0
642 [ "${_hr#*[!0-9a-f]}" != "$_hr" ] || [ "${_hr#*[!0]}" = "$_hr" ] || [ "${#_hr}" -lt 40 ] || return 1
643 return 0
646 # List all Git repositories, with given prefix if specified, one-per-line
647 # All project names starting with _ are always excluded from the result
648 get_repo_list() {
649 if [ -n "$1" ]; then
650 LC_ALL=C cut -d : -f 1,3 "$cfg_chroot"/etc/group | LC_ALL=C grep "^$1"
651 else
652 LC_ALL=C cut -d : -f 1,3 "$cfg_chroot"/etc/group
653 fi | while IFS=: read name id; do
654 [ $id -lt 65536 ] || case "$name" in _*) :;; ?*) echo "$name"; esac
655 done
658 # set the variable named by the first argument to the project part (i.e. WITH
659 # the trailing ".git" but WITHOUT the leading $cfg_reporoot) of the directory
660 # specified by the second argument.
661 # This function cannot be fooled by symbolic links.
662 # If the second argument is omitted (or empty) use $(pwd -P) instead.
663 # The directory specified by the second argument must exist.
664 v_get_proj_from_dir() {
665 [ -n "$2" ] || set -- "$1" "$(pwd -P)"
666 [ -d "$2" ] || return 1
667 case "$2" in
668 "$cfg_reporoot/"?*)
669 # Simple case that does not need any fancy footwork
670 _projpart="${2#$cfg_reporoot/}"
673 _absrr="$(cd "$cfg_reporoot" && pwd -P)"
674 _abspd="$(cd "$2" && pwd -P)"
675 case "$_abspd" in
676 "$_absrr/"?*)
677 # The normal case
678 _projpart="${_abspd#$_absrr/}"
681 # Must have been reached via a symbolic link, but
682 # we have no way to know the source, so use a
683 # generic "_external" leader combined with just the
684 # trailing directory name
685 _abspd="${_abspd%/}"
686 _abspd="${_abspd%/.git}"
687 _projpart="_external/${_abspd##*/}"
689 esac
690 esac
691 eval "$1="'"$_projpart"'
694 # Returns success if "$1" does not exist or contains only blank lines and comments
695 # The parsing rules are in Git's sha1-file.c parse_alt_odb_entry function;
696 # the format for blank lines and comments has been the same since Git v0.99.5
697 is_empty_alternates_file() {
698 [ -n "$1" ] || return 0
699 [ -e "$1" ] && [ -f "$1" ] && [ -s "$1" ] || return 0
700 [ -r "$1" ] || return 1
701 LC_ALL=C awk <"$1" '!/^$/ && !/^#/ {exit 1}'
704 # Return success if the given project name has at least one immediate child fork
705 # that has a non-empty alternates file
706 has_forks_with_alternates() {
707 _prj="${1%.git}"
708 [ -n "$_prj" ] || return 1
709 [ -d "$cfg_reporoot/$_prj" ] || return 1
710 is_git_dir "$cfg_reporoot/$_prj.git" || return 1
712 get_repo_list "$_prj/[^/:][^/:]*:" |
713 while read -r _prjname && [ -n "$_prjname" ]; do
714 is_empty_alternates_file "$cfg_reporoot/$_prjname.git/objects/info/alternates" ||
715 exit 1 # will only exit implicit subshell created by '|'
716 done
717 then
718 return 1
720 return 0
723 # returns empty string and error for empty string otherwise one of
724 # m => normal Git mirror
725 # s => mirror from svn source
726 # d => mirror from darcs source
727 # b => mirror from bzr source
728 # h => mirror from hg source
729 # w => mirror from mediawiki source
730 # f => mirror from other fast-import source
731 # note that if the string is non-empty and none of s, d, b or h match the
732 # return will always be type m regardless of whether it's a valid Git URL
733 get_url_mirror_type() {
734 case "$1" in
736 return 1
738 svn://* | svn+http://* | svn+https://* | svn+file://* | svn+ssh://*)
739 echo 's'
741 darcs://* | darcs+http://* | darcs+https://*)
742 echo 'd'
744 bzr://*)
745 echo 'b'
747 hg+http://* | hg+https://* | hg+file://* | hg+ssh://* | hg::*)
748 echo 'h'
750 mediawiki::*)
751 echo 'w'
754 echo 'm'
756 esac
757 return 0
760 # returns false for empty string
761 # returns true if the passed in url is a mirror using git fast-import
762 is_gfi_mirror_url() {
763 [ -n "$1" ] || return 1
764 case "$(get_url_mirror_type "$1" 2>/dev/null || :)" in
765 d|b|h|w|f)
766 # darcs, bzr, hg and mediawiki mirrors use git fast-import
767 # and so do generic "f" fast-import mirrors
768 return 0
771 # Don't think git-svn currently uses git fast-import
772 # And Git mirrors certainly do not
773 return 1
775 esac
776 # assume it does not use git fast-import
777 return 1
780 # returns false for empty string
781 # returns true if the passed in url is a mirror using git-svn
782 is_svn_mirror_url() {
783 [ -n "$1" ] || return 1
784 [ "$(get_url_mirror_type "$1" 2>/dev/null || :)" = "s" ]
787 # returns mirror url for gitweb.baseurl of git directory
788 # (GIT_DIR) passed in as the argument (which defaults to "." if omitted)
789 # will fail if the directory does not have .nofetch and gitweb.baseurl
790 # comes back empty -- otherwise .nofetch directories succeed with a "" return
791 # automatically strips any leading "disabled " prefix before returning result
792 get_mirror_url() {
793 _gitdir="${1:-.}"
794 # always return empty for non-mirrors
795 ! [ -e "$_gitdir/.nofetch" ] || return 0
796 _url="$(GIT_DIR="$_gitdir" config_get baseurl 2>/dev/null)" || :
797 _url="${_url##* }"
798 [ -n "$_url" ] || return 1
799 printf '%s\n' "$_url"
800 return 0
803 # returns get_url_mirror_type for gitweb.baseurl of git directory
804 # (GIT_DIR) passed in as the argument (which defaults to "." if omitted)
805 # will fail if the directory does not have .nofetch and gitweb.baseurl
806 # comes back empty -- otherwise .nofetch directories succeed with a "" return
807 # automatically strips any leading "disabled " prefix before testing
808 get_mirror_type() {
809 _url="$(get_mirror_url "$@")" || return 1
810 [ -n "$_url" ] || return 0
811 get_url_mirror_type "$_url"
814 # returns true if the passed in git dir (defaults to ".") is a mirror using git fast-import
815 is_gfi_mirror() {
816 _url="$(get_mirror_url "$@")" || return 1
817 is_gfi_mirror_url "$_url"
820 # returns true if the passed in git dir (defaults to ".") is a mirror using git-svn
821 is_svn_mirror() {
822 _url="$(get_mirror_url "$@")" || return 1
823 is_svn_mirror_url "$_url"
826 # current directory must already be set to Git repository
827 # if girocco.headok is already true succeeds without doing anything
828 # if rev-parse --verify HEAD succeeds sets headok=true and succeeds
829 # otherwise tries to set HEAD to a symbolic ref to refs/heads/master
830 # then refs/heads/trunk and finally the first top-level head from
831 # refs/heads/* (i.e. only two slashes in the name) and finally any
832 # existing refs/heads. The first one to succeed wins and sets headok=true
833 # and then a successful exit. Otherwise headok is left unset with a failure exit
834 # We use the girocco.headok flag to make sure we only force a valid HEAD symref
835 # when the repository is being set up -- if the HEAD is later deleted (through
836 # a push or fetch --prune) that's no longer our responsibility to fix
837 check_and_set_head() {
838 [ "$(git config --bool girocco.headok 2>/dev/null || :)" != "true" ] || return 0
839 if git rev-parse --verify --quiet HEAD >/dev/null; then
840 git config --bool girocco.headok true
841 return 0
843 for _hr in refs/heads/master refs/heads/trunk; do
844 if git rev-parse --verify --quiet "$_hr" >/dev/null; then
845 _update_head_symref "$_hr"
846 return 0
848 done
849 git for-each-ref --format="%(refname)" refs/heads 2>/dev/null |
850 while read -r _hr; do
851 case "${_hr#refs/heads/}" in */*) :;; *)
852 _update_head_symref "$_hr"
853 exit 1 # exit subshell created by "|"
854 esac
855 done || return 0
856 _hr="$(git for-each-ref --format="%(refname)" refs/heads 2>/dev/null | head -n 1)" || :
857 if [ -n "$_hr" ]; then
858 _update_head_symref "$_hr"
859 return 0
861 return 1
863 _update_head_symref() {
864 git symbolic-ref HEAD "$1"
865 git config --bool girocco.headok true
866 ! [ -d htmlcache ] || { >htmlcache/changed; } 2>/dev/null || :
869 # current directory must already be set to Git repository
870 # if the directory needs to have gc run and .needsgc is not already set
871 # then .needsgc will be set triggering a "mini" gc at the next opportunity
872 # Girocco shouldn't generate any loose objects but we check for that anyway
873 check_and_set_needsgc() {
874 # If there's a .needspack file and ANY loose objects with a newer timestamp
875 # then also set .needsgc otherwise remove it. The only caller that may set
876 # .needspack is a mirror therefore we don't have to worry about removing a
877 # .needspack out from under a simultaneous creator. We always do this and
878 # do it first to try and avoid leaving a stale .needspack lying around.
879 if [ -e .needspack ]; then
880 _objfiles=
881 _objfiles="$(( $(find -L objects/$octet -maxdepth 1 -newer .needspack -name "$octet19*" -type f -print 2>/dev/null |
882 head -n 1 | LC_ALL=C wc -l) +0 ))"
883 if [ "${_objfiles:-0}" = "0" ]; then
884 rm -f .needspack
885 else
886 [ -e .needsgc ] || >.needsgc
889 ! [ -e .needsgc ] || return 0
890 _packs=
891 { _packs="$(list_packs --quiet --count --exclude-no-idx --exclude-keep objects/pack || :)" || :; } 2>/dev/null
892 if [ "${_packs:-0}" -ge 20 ]; then
893 >.needsgc
894 return 0
896 _logfiles=
897 { _logfiles="$(($(find -L reflogs -maxdepth 1 -type f -print | wc -l || :)+0))" || :; } 2>/dev/null
898 if [ "${_logfiles:-0}" -ge 50 ]; then
899 >.needsgc
900 return 0
902 # Truly git gc only checks the number of objects in the objects/17 directory
903 # We check for -ge 10 which should make the probability of having more than
904 # 5120 (20*256) loose objects present when there are less than 10 in
905 # objects/17 vanishingly small (20 is the threshold we use for pack files)
906 _objfiles=
907 ! [ -d objects/17 ] ||
908 { _objfiles="$(($(find -L objects/17 -type f -name "$octet19*" -print | wc -l || :)+0))" || :; } 2>/dev/null
909 if [ "${_objfiles:-0}" -ge 10 ]; then
910 >.needsgc
911 return 0
915 # A well-known UTF-8 locale is required for some of the fast-import providers
916 # in order to avoid mangling characters. Ideally we could use "POSIX.UTF-8"
917 # but that is not reliably UTF-8 but rather usually US-ASCII.
918 # We parse the output of `locale -a` and select a suitable UTF-8 locale at
919 # install time and store that in $var_utf8_locale if one is found.
920 # If we cannot find one in the `locale -a` output then we just use a well-known
921 # UTF-8 locale and hope for the best. We set LC_ALL to our choice and export
922 # it. We only set this temporarily when running the fast-import providers.
923 set_utf8_locale() {
924 LC_ALL="${var_utf8_locale:-en_US.UTF-8}"
925 export LC_ALL
928 # hg-fast-export | git fast-import with error handling in current directory GIT_DIR
929 git_hg_fetch() (
930 set_utf8_locale
931 _python="${PYTHON:-python}"
932 rm -f hg2git-marks.old hg2git-marks.new
933 if [ -f hg2git-marks ] && [ -s hg2git-marks ]; then
934 LC_ALL=C sed 's/^:\([^ ][^ ]*\) \([^ ][^ ]*\)$/\2 \1/' <hg2git-marks | {
935 if [ -n "$var_have_git_185" ]; then
936 git cat-file --batch-check=':%(rest) %(objectname)'
937 else
938 LC_ALL=C sed 's/^\([^ ][^ ]*\) \([^ ][^ ]*\)$/:\2 \1/'
940 } | LC_ALL=C sed '/ missing$/d' >hg2git-marks.old
941 if [ -n "$var_have_git_171" ] &&
942 git rev-parse --quiet --verify refs/notes/hg >/dev/null; then
943 if [ -z "$var_have_git_185" ] ||
944 ! LC_ALL=C cmp -s hg2git-marks hg2git-marks.old; then
945 _nm='hg-fast-export'
946 GIT_AUTHOR_NAME="$_nm"
947 GIT_COMMITTER_NAME="$_nm"
948 GIT_AUTHOR_EMAIL="$_nm"
949 GIT_COMMITTER_EMAIL="$_nm"
950 export GIT_AUTHOR_NAME
951 export GIT_COMMITTER_NAME
952 export GIT_AUTHOR_EMAIL
953 export GIT_COMMITTER_EMAIL
954 git notes --ref=refs/notes/hg prune
955 unset GIT_AUTHOR_NAME
956 unset GIT_COMMITTER_NAME
957 unset GIT_AUTHOR_EMAIL
958 unset GIT_COMMITTER_EMAIL
961 else
962 >hg2git-marks.old
964 _err1=
965 _err2=
966 exec 3>&1
967 { read -r _err1 || :; read -r _err2 || :; } <<-EOT
969 exec 4>&3 3>&1 1>&4 4>&-
971 _e1=0
972 _af="$(git config hg.authorsfile)" || :
973 _cmd='GIT_DIR="$(pwd)" "$_python" "$cfg_basedir/bin/hg-fast-export.py" \
974 --repo "$(pwd)/repo.hg" \
975 --marks "$(pwd)/hg2git-marks.old" \
976 --mapping "$(pwd)/hg2git-mapping" \
977 --heads "$(pwd)/hg2git-heads" \
978 --status "$(pwd)/hg2git-state" \
979 -U unknown --force --flatten --hg-hash'
980 [ -z "$_af" ] || _cmd="$_cmd"' --authors "$_af"'
981 eval "$_cmd" 3>&- || _e1=$?
982 echo $_e1 >&3
985 _e2=0
986 git fast-import \
987 --import-marks="$(pwd)/hg2git-marks.old" \
988 --export-marks="$(pwd)/hg2git-marks.new" \
989 --export-pack-edges="$(pwd)/gfi-packs" \
990 --force 3>&- || _e2=$?
991 echo $_e2 >&3
995 exec 3>&-
996 [ "$_err1" = 0 ] && [ "$_err2" = 0 ] || return 1
997 mv -f hg2git-marks.new hg2git-marks
998 rm -f hg2git-marks.old
999 git for-each-ref --format='%(refname) %(objectname)' refs/heads |
1000 LC_ALL=C sed -e 's,^refs/heads/,:,' >hg2git-heads