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