jailsetup.sh: make sure etc/backups has correct owner
[girocco.git] / shlib.sh
bloba8d496c5d8a1ba25d3ab7682de0d593b2a12bc05
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 # Make sure we have a reproducible environment by using a controlled HOME dir
202 XDG_CONFIG_HOME="$cfg_chroot/var/empty"
203 HOME="$cfg_chroot/etc/girocco"
204 TMPDIR="/tmp"
205 GIT_CONFIG_NOSYSTEM=1
206 GIT_ATTR_NOSYSTEM=1
207 GIT_NO_REPLACE_OBJECTS=1
208 GIT_TERMINAL_PROMPT=0
209 GIT_PAGER="cat"
210 PAGER="cat"
211 GIT_ASKPASS="$cfg_basedir/bin/git-askpass-password"
212 GIT_SVN_NOTTY=1
213 GIROCCO_SUPPRESS_AUTO_GC_UPDATE=1
214 GIT_SSH="$cfg_basedir/bin/git-ssh"
215 SVN_SSH="$cfg_basedir/bin/git-ssh"
216 export XDG_CONFIG_HOME
217 export HOME
218 export TMPDIR
219 export GIT_CONFIG_NOSYSTEM
220 export GIT_ATTR_NOSYSTEM
221 export GIT_NO_REPLACE_OBJECTS
222 export GIT_TERMINAL_PROMPT
223 export GIT_PAGER
224 export PAGER
225 export GIT_ASKPASS
226 export GIT_SVN_NOTTY
227 export GIROCCO_SUPPRESS_AUTO_GC_UPDATE
228 export GIT_SSH
229 export SVN_SSH
230 unset GIT_USER_AGENT
231 unset GIT_HTTP_USER_AGENT
232 if [ -n "$defined_cfg_git_client_ua" ]; then
233 GIT_USER_AGENT="$cfg_git_client_ua"
234 export GIT_USER_AGENT
236 unset GIT_CONFIG_PARAMETERS
239 ## IMPORTANT!
241 ## Keep gitweb/gitweb_config.perl in sync with these git_add_config calls
242 ## Keep bin/git-shell-verify in sync with these git_add_config calls
244 git_add_config "core.ignoreCase=false"
245 git_add_config "core.pager=cat"
246 if [ -n "$cfg_git_no_mmap" ]; then
247 # Just like compiling with NO_MMAP
248 git_add_config "core.packedGitWindowSize=1m"
249 else
250 # Always use the 32-bit default (32m) even on 64-bit to avoid memory blowout
251 git_add_config "core.packedGitWindowSize=32m"
253 # Always use the 32-bit default (256m) even on 64-bit to avoid memory blowout
254 git_add_config "core.packedGitLimit=256m"
255 [ -z "$var_big_file_threshold" ] ||
256 git_add_config "core.bigFileThreshold=$var_big_file_threshold"
257 git_add_config "gc.auto=0"
259 # Make sure any sendmail.pl config is always available
260 unset SENDMAIL_PL_HOST
261 unset SENDMAIL_PL_PORT
262 unset SENDMAIL_PL_NCBIN
263 unset SENDMAIL_PL_NCOPT
264 [ -z "$cfg_sendmail_pl_host" ] || { SENDMAIL_PL_HOST="$cfg_sendmail_pl_host" && export SENDMAIL_PL_HOST; }
265 [ -z "$cfg_sendmail_pl_port" ] || { SENDMAIL_PL_PORT="$cfg_sendmail_pl_port" && export SENDMAIL_PL_PORT; }
266 [ -z "$cfg_sendmail_pl_ncbin" ] || { SENDMAIL_PL_NCBIN="$cfg_sendmail_pl_ncbin" && export SENDMAIL_PL_NCBIN; }
267 [ -z "$cfg_sendmail_pl_ncopt" ] || { SENDMAIL_PL_NCOPT="$cfg_sendmail_pl_ncopt" && export SENDMAIL_PL_NCOPT; }
269 # Set PATH and PYTHON to the values set by Config.pm, if any
270 unset PYTHON
271 [ -z "$cfg_python" ] || { PYTHON="$cfg_python" && export PYTHON; }
272 [ -z "$cfg_path" ] || { orig_path="$PATH" && PATH="$cfg_path" && export PATH; }
274 # Extra GIT variables that generally ought to be cleared, but whose clearing
275 # could potentially interfere with the correct operation of hook scripts so
276 # they are segregated into a separate function for use as appropriate
277 clean_git_env() {
278 unset GIT_ALTERNATE_OBJECT_DIRECTORIES
279 unset GIT_CONFIG
280 unset GIT_DIR
281 unset GIT_GRAFT_FILE
282 unset GIT_INDEX_FILE
283 unset GIT_OBJECT_DIRECTORY
284 unset GIT_NAMESPACE
287 # We cannot use a git() {} or nc_openbsd() {} function to redirect git
288 # and nc_openbsd to the desired executables because when using
289 # "ENV_VAR=xxx func" the various /bin/sh implementations behave in various
290 # different and unexpected ways:
291 # a) treat "ENV_VAR=xxx" like a separate, preceding "export ENV_VAR=xxx"
292 # b) treat "ENV_VAR=xxx" like a separate, prededing "ENV_VAR=xxx"
293 # c) treat "ENV_VAR=xxx" like a temporary setting only while running func
294 # None of these are good. We want a temporary "export ENV_VAR=xxx"
295 # setting only while running func which none of the /bin/sh's do.
297 # Instead we'd like to use an alias that provides the desired behavior without
298 # any of the bad (a), (b) or (c) effects.
300 # However, unfortunately, some of the crazy /bin/sh implementations do not
301 # recognize alias expansions when preceded by variable assignments!
303 # So we are left with git() {} and nc_openbsd() {} functions and in the
304 # case of git() {} we can compensate for (b) and (c) failing to export
305 # but not (a) and (b) persisting the values so the caller will simply
306 # have to beware and explicitly unset any variables that should not persist
307 # beyond the function call itself.
309 git() (
310 [ z"${GIT_DIR+set}" != z"set" ] || export GIT_DIR
311 [ z"${GIT_SSL_NO_VERIFY+set}" != z"set" ] || export GIT_SSL_NO_VERIFY
312 [ z"${GIT_TRACE_PACKET+set}" != z"set" ] || export GIT_TRACE_PACKET
313 [ z"${GIT_USER_AGENT+set}" != z"set" ] || export GIT_USER_AGENT
314 [ z"${GIT_HTTP_USER_AGENT+set}" != z"set" ] || export GIT_HTTP_USER_AGENT
315 exec "$cfg_git_bin" "$@"
318 # Since we do not yet require at least Git 1.8.5 this is a compatibility function
319 # that allows us to use git update-ref --stdin where supported and the slow shell
320 # script where not, but only the "delete" operation is currently supported.
321 git_updateref_stdin() {
322 if [ -n "$var_have_git_185" ]; then
323 git update-ref --stdin
324 else
325 while read -r _op _ref; do
326 case "$_op" in
327 delete)
328 git update-ref -d "$_ref"
331 echo "bad git_updateref_stdin op: $_op" >&2
332 exit 1
334 esac
335 done
339 # see comments for git() -- callers must explicitly export all variables
340 # intended for the commands these functions run before calling them
341 perl() { command "${var_perl_bin:-perl}" "$@"; }
342 gzip() { command "${var_gzip_bin:-gzip}" "$@"; }
344 nc_openbsd() { command "$var_nc_openbsd_bin" "$@"; }
346 list_packs() { command "$cfg_basedir/bin/list_packs" "$@"; }
348 readlink() { command "$cfg_basedir/bin/readlink" "$@"; }
350 strftime() { command "$cfg_basedir/bin/strftime" "$@"; }
352 # Some platforms' broken xargs runs the command always at least once even if
353 # there's no input unless given a special option. Automatically supply the
354 # option on those platforms by providing an xargs function.
355 xargs() { command xargs $var_xargs_r "$@"; }
357 _addrlist() {
358 _list=
359 for _addr in "$@"; do
360 [ -z "$_list" ] || _list="$_list, "
361 _list="$_list$_addr"
362 done
363 echo "$_list"
366 _sendmail() {
367 _mailer="${cfg_sendmail_bin:-/usr/sbin/sendmail}"
368 if [ -n "$cfg_sender" ]; then
369 "$_mailer" -i -f "$cfg_sender" "$@"
370 else
371 "$_mailer" -i "$@"
375 # First argument is an id WITHOUT surrounding '<' and '>' to use in a
376 # "References:" header. It may be "" to suppress the "References" header.
377 # Following arguments are just like mail function
378 mailref() {
379 _references=
380 if [ $# -ge 1 ]; then
381 _references="$1"
382 shift
384 _subject=
385 if [ "$1" = "-s" ]; then
386 shift
387 _subject="$1"
388 shift
391 echo "From: \"$cfg_name\" ($cfg_title) <$cfg_admin>"
392 echo "To: $(_addrlist "$@")"
393 [ -z "$_subject" ] || echo "Subject: $_subject"
394 echo "MIME-Version: 1.0"
395 echo "Content-Type: text/plain; charset=utf-8; format=fixed"
396 echo "Content-Transfer-Encoding: 8bit"
397 [ -z "$_references" ] || echo "References: <$_references>"
398 [ -n "$cfg_suppress_x_girocco" ] || echo "X-Girocco: $cfg_gitweburl"
399 echo "Auto-Submitted: auto-generated"
400 echo ""
402 } | _sendmail "$@"
405 # Usage: mail [-s <subject>] <addr> [<addr>...]
406 mail() {
407 mailref "" "$@"
410 # bang CMD... will execute the command with well-defined failure mode;
411 # set bang_action to string of the failed action ('clone', 'update', ...);
412 # re-define the bang_trap() function to do custom cleanup before bailing out
413 bang() {
414 bang_errcode=
415 bang_catch "$@"
416 [ "${bang_errcode:-0}" = "0" ] || bang_failed
419 bang_catch() {
420 bang_active=1
421 bang_cmd="$*"
422 bang_errcode=0
423 if [ "${show_progress:-0}" != "0" ]; then
424 exec 3>&1
425 read -r bang_errcode <<-EOT || :
427 exec 4>&3 3>&1 1>&4 4>&-
428 { "$@" 3>&- || echo $? >&3; } 2>&1 | tee -i -a "$bang_log"
431 exec 3>&-
432 if [ -z "$bang_errcode" ] || [ "$bang_errcode" = "0" ]; then
433 # All right. Cool.
434 bang_active=
435 bang_cmd=
436 return;
438 else
439 if "$@" >>"$bang_log" 2>&1; then
440 # All right. Cool.
441 bang_active=
442 bang_cmd=
443 return;
444 else
445 bang_errcode="$?"
450 bang_failed() {
451 bang_active=
452 unset GIT_DIR
453 >.banged
454 cat "$bang_log" >.banglog
455 echo "" >>.banglog
456 echo "$bang_cmd failed with error code $bang_errcode" >>.banglog
457 ! [ -d htmlcache ] || { >htmlcache/changed; } 2>/dev/null || :
458 if [ "${show_progress:-0}" != "0" ]; then
459 echo ""
460 echo "$bang_cmd failed with error code $bang_errcode"
462 if [ -e .bangagain ]; then
463 git config --remove-section girocco.bang 2>/dev/null || :
464 rm -f .bangagain
466 bangcount="$(git config --int girocco.bang.count 2>/dev/null)" || :
467 bangcount=$(( ${bangcount:-0} + 1 ))
468 git config --int girocco.bang.count $bangcount
469 if [ $bangcount -eq 1 ]; then
470 git config girocco.bang.firstfail "$(TZ=UTC date "+%Y-%m-%d %T UTC")"
472 if [ $bangcount -ge $cfg_min_mirror_failure_message_count ] &&
473 [ "$(git config --bool girocco.bang.messagesent 2>/dev/null || :)" != "true" ] &&
474 ! check_interval "girocco.bang.firstfail" $cfg_min_mirror_failure_message_interval; then
475 bangmailok="$(git config --bool gitweb.statusupdates 2>/dev/null || echo true)"
476 bangaddrs=
477 [ "$bangmailok" = "false" ] || [ -z "$mail" ] || bangaddrs="$mail"
478 [ -z "$cfg_admincc" ] || [ "$cfg_admincc" = "0" ] || [ -z "$cfg_admin" ] ||
479 if [ -z "$bangaddrs" ]; then bangaddrs="$cfg_admin"; else bangaddrs="$bangaddrs,$cfg_admin"; fi
480 rsubj=
481 [ $bangcount -le 1 ] || rsubj=" repeatedly"
482 [ -z "$bangaddrs" ] ||
484 echo "$bang_cmd failed with error code $bang_errcode"
485 echo ""
486 rsubj=
487 if [ $bangcount -gt 1 ]; then
488 echo "$bangcount consecutive update failures have occurred since $(config_get girocco.bang.firstfail)"
489 echo ""
491 echo "you will not receive any more notifications until recovery"
492 echo "this status message may be disabled on the project admin page"
493 echo ""
494 echo "Log follows:"
495 echo ""
496 cat "$bang_log"
497 } | mailref "update@$cfg_gitweburl/$proj.git" -s "[$cfg_name] $proj $bang_action failed$rsubj" "$bangaddrs"
498 git config --bool girocco.bang.messagesent true
500 bangthrottle=
501 [ $bangcount -lt 15 ] ||
502 check_interval "girocco.bang.firstfail" $(( $cfg_min_mirror_interval * 3 / 2 )) ||
503 bangthrottle=1
504 bang_trap $bangthrottle
505 [ -n "$bang_errcode" ] && [ "$bang_errcode" != "0" ] || bang_errcode=1
506 exit $bang_errcode
509 # bang_eval CMD... will evaluate the command with well-defined failure mode;
510 # Identical to bang CMD... except the command is eval'd instead of executed.
511 bang_eval() {
512 bang eval "$*"
515 # Default bang settings:
516 bang_setup() {
517 bang_active=
518 bang_action="lame_programmer"
519 bang_trap() { :; }
520 bang_tmpdir="${TMPDIR:-/tmp}"
521 bang_tmpdir="${bang_tmpdir%/}"
522 bang_log="$(mktemp "${bang_tmpdir:-/tmp}/repomgr-XXXXXX")"
523 is_git_dir . || {
524 echo "bang_setup called with current directory not a git directory" >&2
525 exit 1
527 trap 'rm -f "$bang_log"' EXIT
528 trap '[ -z "$bang_active" ] || { bang_errcode=130; bang_failed; }; exit 130' INT
529 trap '[ -z "$bang_active" ] || { bang_errcode=143; bang_failed; }; exit 143' TERM
532 # Remove banged status
533 bang_reset() {
534 rm -f .banged .bangagain .banglog
535 git config --remove-section girocco.bang 2>/dev/null || :
538 # Check to see if banged status
539 is_banged() {
540 [ -e .banged ]
543 # Check to see if banged message was sent
544 was_banged_message_sent() {
545 [ "$(git config --bool girocco.bang.messagesent 2>/dev/null || :)" = "true" ]
548 # Progress report - if show_progress is set, shows the given message.
549 progress() {
550 [ "${show_progress:-0}" = "0" ] || echo "$*"
553 # Project config accessors; must be run in project directory
554 config_get() {
555 case "$1" in
556 *.*)
557 git config "$1";;
559 git config "gitweb.$1";;
560 esac
563 config_set() {
564 git config "gitweb.$1" "$2" && chgrp $var_group config && chmod g+w config
567 config_set_raw() {
568 git config "$1" "$2" && chgrp $var_group config && chmod g+w config
571 config_get_date_seconds() {
572 _dt="$(config_get "$1")" || :
573 [ -n "$_dt" ] || return 1
574 _ds="$(perl -I@basedir@ -MGirocco::Util -e "print parse_any_date('$_dt')")"
575 [ -n "$_ds" ] || return 1
576 echo "$_ds"
579 # Tool for checking whether given number of seconds has not passed yet
580 check_interval() {
581 os="$(config_get_date_seconds "$1")" || return 1
582 ns="$(date +%s)"
583 [ $ns -lt $(($os+$2)) ]
586 # Check if we are running with effective root permissions
587 is_root() {
588 [ "$(id -u 2>/dev/null)" = "0" ]
591 # Check to see if the single argument (default ".") is a Git directory
592 is_git_dir() {
593 # Just like Git's test except we ignore GIT_OBJECT_DIRECTORY
594 # And we are slightly more picky (must be refs/.+ not refs/.*)
595 [ $# -ne 0 ] || set -- "."
596 [ -d "$1/objects" ] && [ -x "$1/objects" ] || return 1
597 [ -d "$1/refs" ] && [ -x "$1/refs" ] || return 1
598 if [ -L "$1/HEAD" ]; then
599 _hr="$(readlink "$1/HEAD")"
600 case "$_hr" in "refs/"?*) :;; *) return 1;; esac
602 [ -f "$1/HEAD" ] && [ -r "$1/HEAD" ] || return 1
603 read -r _hr <"$1/HEAD" || return 1
604 case "$_hr" in
605 $octet20*)
606 [ "${_hr#*[!0-9a-f]}" = "$_hr" ] || return 1
607 return 0;;
608 ref:refs/?*)
609 return 0;;
610 ref:*)
611 _hr="${_hr##ref:*[ $tab]}"
612 case "$_hr" in "refs/"?*) return 0;; esac
613 esac
614 return 1
617 # Check to see if the single argument (default ".") is a directory with no refs
618 is_empty_refs_dir() {
619 [ $# -ne 0 ] || set -- "."
620 if [ -s "$1/packed-refs" ]; then
621 # could be a packed-refs file with just a '# pack-refs ..." line
622 # null hash lines and peel lines do not count either
623 _refcnt="$(( $(LC_ALL=C sed <"$1/packed-refs" \
624 -e "/^00* /d" \
625 -e "/^$octet20$hexdig* refs\/[^ $tab]*\$/!d" | wc -l) ))"
626 [ "${_refcnt:-0}" -eq 0 ] || return 1
628 if [ -d "$1/refs" ]; then
629 # quick and dirty check, doesn't try to validate contents
630 # or ignore embedded symbolic refs
631 _refcnt="$(( $(find -L "$1/refs" -type f -print 2>/dev/null | head -n 1 | LC_ALL=C wc -l) ))"
632 [ "${_refcnt:-0}" -eq 0 ] || return 1
634 # last chance a detached HEAD (we ignore any linked working trees though)
635 [ -s "$1/HEAD" ] && read -r _hr <"$1/HEAD" && [ -n "$_hr" ] || return 0
636 [ "${_hr#*[!0-9a-f]}" != "$_hr" ] || [ "${_hr#*[!0]}" = "$_hr" ] || [ "${#_hr}" -lt 40 ] || return 1
637 return 0
640 # List all Git repositories, with given prefix if specified, one-per-line
641 # All project names starting with _ are always excluded from the result
642 get_repo_list() {
643 if [ -n "$1" ]; then
644 LC_ALL=C cut -d : -f 1,3 "$cfg_chroot"/etc/group | LC_ALL=C grep "^$1"
645 else
646 LC_ALL=C cut -d : -f 1,3 "$cfg_chroot"/etc/group
647 fi | while IFS=: read name id; do
648 [ $id -lt 65536 ] || case "$name" in _*) :;; ?*) echo "$name"; esac
649 done
652 # set the variable named by the first argument to the project part (i.e. WITH
653 # the trailing ".git" but WITHOUT the leading $cfg_reporoot) of the directory
654 # specified by the second argument.
655 # This function cannot be fooled by symbolic links.
656 # If the second argument is omitted (or empty) use $(pwd -P) instead.
657 # The directory specified by the second argument must exist.
658 v_get_proj_from_dir() {
659 [ -n "$2" ] || set -- "$1" "$(pwd -P)"
660 [ -d "$2" ] || return 1
661 case "$2" in
662 "$cfg_reporoot/"?*)
663 # Simple case that does not need any fancy footwork
664 _projpart="${2#$cfg_reporoot/}"
667 _absrr="$(cd "$cfg_reporoot" && pwd -P)"
668 _abspd="$(cd "$2" && pwd -P)"
669 case "$_abspd" in
670 "$_absrr/"?*)
671 # The normal case
672 _projpart="${_abspd#$_absrr/}"
675 # Must have been reached via a symbolic link, but
676 # we have no way to know the source, so use a
677 # generic "_external" leader combined with just the
678 # trailing directory name
679 _abspd="${_abspd%/}"
680 _abspd="${_abspd%/.git}"
681 _projpart="_external/${_abspd##*/}"
683 esac
684 esac
685 eval "$1="'"$_projpart"'
688 # Returns success if "$1" does not exist or contains only blank lines and comments
689 # The parsing rules are in Git's sha1-file.c parse_alt_odb_entry function;
690 # the format for blank lines and comments has been the same since Git v0.99.5
691 is_empty_alternates_file() {
692 [ -n "$1" ] || return 0
693 [ -e "$1" ] && [ -f "$1" ] && [ -s "$1" ] || return 0
694 [ -r "$1" ] || return 1
695 LC_ALL=C awk <"$1" '!/^$/ && !/^#/ {exit 1}'
698 # Return success if the given project name has at least one immediate child fork
699 # that has a non-empty alternates file
700 has_forks_with_alternates() {
701 _prj="${1%.git}"
702 [ -n "$_prj" ] || return 1
703 [ -d "$cfg_reporoot/$_prj" ] || return 1
704 is_git_dir "$cfg_reporoot/$_prj.git" || return 1
706 get_repo_list "$_prj/[^/:][^/:]*:" |
707 while read -r _prjname && [ -n "$_prjname" ]; do
708 is_empty_alternates_file "$cfg_reporoot/$_prjname.git/objects/info/alternates" ||
709 exit 1 # will only exit implicit subshell created by '|'
710 done
711 then
712 return 1
714 return 0
717 # returns empty string and error for empty string otherwise one of
718 # m => normal Git mirror
719 # s => mirror from svn source
720 # d => mirror from darcs source
721 # b => mirror from bzr source
722 # h => mirror from hg source
723 # w => mirror from mediawiki source
724 # f => mirror from other fast-import source
725 # note that if the string is non-empty and none of s, d, b or h match the
726 # return will always be type m regardless of whether it's a valid Git URL
727 get_url_mirror_type() {
728 case "$1" in
730 return 1
732 svn://* | svn+http://* | svn+https://* | svn+file://* | svn+ssh://*)
733 echo 's'
735 darcs://* | darcs+http://* | darcs+https://*)
736 echo 'd'
738 bzr://*)
739 echo 'b'
741 hg+http://* | hg+https://* | hg+file://* | hg+ssh://* | hg::*)
742 echo 'h'
744 mediawiki::*)
745 echo 'w'
748 echo 'm'
750 esac
751 return 0
754 # returns false for empty string
755 # returns true if the passed in url is a mirror using git fast-import
756 is_gfi_mirror_url() {
757 [ -n "$1" ] || return 1
758 case "$(get_url_mirror_type "$1" 2>/dev/null || :)" in
759 d|b|h|w|f)
760 # darcs, bzr, hg and mediawiki mirrors use git fast-import
761 # and so do generic "f" fast-import mirrors
762 return 0
765 # Don't think git-svn currently uses git fast-import
766 # And Git mirrors certainly do not
767 return 1
769 esac
770 # assume it does not use git fast-import
771 return 1
774 # returns false for empty string
775 # returns true if the passed in url is a mirror using git-svn
776 is_svn_mirror_url() {
777 [ -n "$1" ] || return 1
778 [ "$(get_url_mirror_type "$1" 2>/dev/null || :)" = "s" ]
781 # returns mirror url for gitweb.baseurl of git directory
782 # (GIT_DIR) passed in as the argument (which defaults to "." if omitted)
783 # will fail if the directory does not have .nofetch and gitweb.baseurl
784 # comes back empty -- otherwise .nofetch directories succeed with a "" return
785 # automatically strips any leading "disabled " prefix before returning result
786 get_mirror_url() {
787 _gitdir="${1:-.}"
788 # always return empty for non-mirrors
789 ! [ -e "$_gitdir/.nofetch" ] || return 0
790 _url="$(GIT_DIR="$_gitdir" config_get baseurl 2>/dev/null)" || :
791 _url="${_url##* }"
792 [ -n "$_url" ] || return 1
793 printf '%s\n' "$_url"
794 return 0
797 # returns get_url_mirror_type for gitweb.baseurl of git directory
798 # (GIT_DIR) passed in as the argument (which defaults to "." if omitted)
799 # will fail if the directory does not have .nofetch and gitweb.baseurl
800 # comes back empty -- otherwise .nofetch directories succeed with a "" return
801 # automatically strips any leading "disabled " prefix before testing
802 get_mirror_type() {
803 _url="$(get_mirror_url "$@")" || return 1
804 [ -n "$_url" ] || return 0
805 get_url_mirror_type "$_url"
808 # returns true if the passed in git dir (defaults to ".") is a mirror using git fast-import
809 is_gfi_mirror() {
810 _url="$(get_mirror_url "$@")" || return 1
811 is_gfi_mirror_url "$_url"
814 # returns true if the passed in git dir (defaults to ".") is a mirror using git-svn
815 is_svn_mirror() {
816 _url="$(get_mirror_url "$@")" || return 1
817 is_svn_mirror_url "$_url"
820 # current directory must already be set to Git repository
821 # if girocco.headok is already true succeeds without doing anything
822 # if rev-parse --verify HEAD succeeds sets headok=true and succeeds
823 # otherwise tries to set HEAD to a symbolic ref to refs/heads/master
824 # then refs/heads/trunk and finally the first top-level head from
825 # refs/heads/* (i.e. only two slashes in the name) and finally any
826 # existing refs/heads. The first one to succeed wins and sets headok=true
827 # and then a successful exit. Otherwise headok is left unset with a failure exit
828 # We use the girocco.headok flag to make sure we only force a valid HEAD symref
829 # when the repository is being set up -- if the HEAD is later deleted (through
830 # a push or fetch --prune) that's no longer our responsibility to fix
831 check_and_set_head() {
832 [ "$(git config --bool girocco.headok 2>/dev/null || :)" != "true" ] || return 0
833 if git rev-parse --verify --quiet HEAD >/dev/null; then
834 git config --bool girocco.headok true
835 return 0
837 for _hr in refs/heads/master refs/heads/trunk; do
838 if git rev-parse --verify --quiet "$_hr" >/dev/null; then
839 _update_head_symref "$_hr"
840 return 0
842 done
843 git for-each-ref --format="%(refname)" refs/heads 2>/dev/null |
844 while read -r _hr; do
845 case "${_hr#refs/heads/}" in */*) :;; *)
846 _update_head_symref "$_hr"
847 exit 1 # exit subshell created by "|"
848 esac
849 done || return 0
850 _hr="$(git for-each-ref --format="%(refname)" refs/heads 2>/dev/null | head -n 1)" || :
851 if [ -n "$_hr" ]; then
852 _update_head_symref "$_hr"
853 return 0
855 return 1
857 _update_head_symref() {
858 git symbolic-ref HEAD "$1"
859 git config --bool girocco.headok true
860 ! [ -d htmlcache ] || { >htmlcache/changed; } 2>/dev/null || :
863 # current directory must already be set to Git repository
864 # if the directory needs to have gc run and .needsgc is not already set
865 # then .needsgc will be set triggering a "mini" gc at the next opportunity
866 # Girocco shouldn't generate any loose objects but we check for that anyway
867 check_and_set_needsgc() {
868 # If there's a .needspack file and ANY loose objects with a newer timestamp
869 # then also set .needsgc otherwise remove it. The only caller that may set
870 # .needspack is a mirror therefore we don't have to worry about removing a
871 # .needspack out from under a simultaneous creator. We always do this and
872 # do it first to try and avoid leaving a stale .needspack lying around.
873 if [ -e .needspack ]; then
874 _objfiles=
875 _objfiles="$(( $(find -L objects/$octet -maxdepth 1 -newer .needspack -name "$octet19*" -type f -print 2>/dev/null |
876 head -n 1 | LC_ALL=C wc -l) +0 ))"
877 if [ "${_objfiles:-0}" = "0" ]; then
878 rm -f .needspack
879 else
880 [ -e .needsgc ] || >.needsgc
883 ! [ -e .needsgc ] || return 0
884 _packs=
885 { _packs="$(list_packs --quiet --count --exclude-no-idx --exclude-keep objects/pack || :)" || :; } 2>/dev/null
886 if [ "${_packs:-0}" -ge 20 ]; then
887 >.needsgc
888 return 0
890 _logfiles=
891 { _logfiles="$(($(find -L reflogs -maxdepth 1 -type f -print | wc -l || :)+0))" || :; } 2>/dev/null
892 if [ "${_logfiles:-0}" -ge 50 ]; then
893 >.needsgc
894 return 0
896 # Truly git gc only checks the number of objects in the objects/17 directory
897 # We check for -ge 10 which should make the probability of having more than
898 # 5120 (20*256) loose objects present when there are less than 10 in
899 # objects/17 vanishingly small (20 is the threshold we use for pack files)
900 _objfiles=
901 ! [ -d objects/17 ] ||
902 { _objfiles="$(($(find -L objects/17 -type f -name "$octet19*" -print | wc -l || :)+0))" || :; } 2>/dev/null
903 if [ "${_objfiles:-0}" -ge 10 ]; then
904 >.needsgc
905 return 0
909 # A well-known UTF-8 locale is required for some of the fast-import providers
910 # in order to avoid mangling characters. Ideally we could use "POSIX.UTF-8"
911 # but that is not reliably UTF-8 but rather usually US-ASCII.
912 # We parse the output of `locale -a` and select a suitable UTF-8 locale at
913 # install time and store that in $var_utf8_locale if one is found.
914 # If we cannot find one in the `locale -a` output then we just use a well-known
915 # UTF-8 locale and hope for the best. We set LC_ALL to our choice and export
916 # it. We only set this temporarily when running the fast-import providers.
917 set_utf8_locale() {
918 LC_ALL="${var_utf8_locale:-en_US.UTF-8}"
919 export LC_ALL
922 # hg-fast-export | git fast-import with error handling in current directory GIT_DIR
923 git_hg_fetch() (
924 set_utf8_locale
925 _python="${PYTHON:-python}"
926 rm -f hg2git-marks.old hg2git-marks.new
927 if [ -f hg2git-marks ] && [ -s hg2git-marks ]; then
928 LC_ALL=C sed 's/^:\([^ ][^ ]*\) \([^ ][^ ]*\)$/\2 \1/' <hg2git-marks | {
929 if [ -n "$var_have_git_185" ]; then
930 git cat-file --batch-check=':%(rest) %(objectname)'
931 else
932 LC_ALL=C sed 's/^\([^ ][^ ]*\) \([^ ][^ ]*\)$/:\2 \1/'
934 } | LC_ALL=C sed '/ missing$/d' >hg2git-marks.old
935 if [ -n "$var_have_git_171" ] &&
936 git rev-parse --quiet --verify refs/notes/hg >/dev/null; then
937 if [ -z "$var_have_git_185" ] ||
938 ! LC_ALL=C cmp -s hg2git-marks hg2git-marks.old; then
939 _nm='hg-fast-export'
940 GIT_AUTHOR_NAME="$_nm"
941 GIT_COMMITTER_NAME="$_nm"
942 GIT_AUTHOR_EMAIL="$_nm"
943 GIT_COMMITTER_EMAIL="$_nm"
944 export GIT_AUTHOR_NAME
945 export GIT_COMMITTER_NAME
946 export GIT_AUTHOR_EMAIL
947 export GIT_COMMITTER_EMAIL
948 git notes --ref=refs/notes/hg prune
949 unset GIT_AUTHOR_NAME
950 unset GIT_COMMITTER_NAME
951 unset GIT_AUTHOR_EMAIL
952 unset GIT_COMMITTER_EMAIL
955 else
956 >hg2git-marks.old
958 _err1=
959 _err2=
960 exec 3>&1
961 { read -r _err1 || :; read -r _err2 || :; } <<-EOT
963 exec 4>&3 3>&1 1>&4 4>&-
965 _e1=0
966 _af="$(git config hg.authorsfile)" || :
967 _cmd='GIT_DIR="$(pwd)" "$_python" "$cfg_basedir/bin/hg-fast-export.py" \
968 --repo "$(pwd)/repo.hg" \
969 --marks "$(pwd)/hg2git-marks.old" \
970 --mapping "$(pwd)/hg2git-mapping" \
971 --heads "$(pwd)/hg2git-heads" \
972 --status "$(pwd)/hg2git-state" \
973 -U unknown --force --flatten --hg-hash'
974 [ -z "$_af" ] || _cmd="$_cmd"' --authors "$_af"'
975 eval "$_cmd" 3>&- || _e1=$?
976 echo $_e1 >&3
979 _e2=0
980 git fast-import \
981 --import-marks="$(pwd)/hg2git-marks.old" \
982 --export-marks="$(pwd)/hg2git-marks.new" \
983 --export-pack-edges="$(pwd)/gfi-packs" \
984 --force 3>&- || _e2=$?
985 echo $_e2 >&3
989 exec 3>&-
990 [ "$_err1" = 0 ] && [ "$_err2" = 0 ] || return 1
991 mv -f hg2git-marks.new hg2git-marks
992 rm -f hg2git-marks.old
993 git for-each-ref --format='%(refname) %(objectname)' refs/heads |
994 LC_ALL=C sed -e 's,^refs/heads/,:,' >hg2git-heads