gc: no waiting to play it again
[girocco.git] / shlib.sh
blobba26b7402d68d59708b3254d12c2dfd5f3d06a44
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 # SHA-1 patterns
7 octet='[0-9a-f][0-9a-f]'
8 octet4="$octet$octet$octet$octet"
9 octet19="$octet4$octet4$octet4$octet4$octet$octet$octet"
10 octet20="$octet4$octet4$octet4$octet4$octet4"
11 nullsha="0000000000000000000000000000000000000000"
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 [ -z "$orig_path" ] || { PATH="$orig_path" && export PATH; }
54 perl -I@basedir@ $__girocco_extrainc -M$__girocco_conf -le \
55 'foreach (sort {uc($a) cmp uc($b)} keys %Girocco::Config::) {
56 my $val = ${$Girocco::Config::{$_}}; defined($val) or $val="";
57 $val =~ s/([\\"\$\`])/\\$1/gos;
58 $val =~ s/(?:\r\n|\r|\n)$//os;
59 print "cfg_$_=\"$val\"";
60 print "defined_cfg_$_=",
61 (defined(${$Girocco::Config::{$_}})?"1":"");
65 # Returns full command path for "$1" if it's a valid command otherwise returns "$1"
66 _fcp() {
67 if _fp="$(command -v "$1" 2>/dev/null)"; then
68 printf '%s\n' "$_fp"
69 else
70 printf '%s\n' "$1"
74 get_girocco_config_var_list() (
75 # Same as get_girocco_config_pm_var_list except that
76 # the following variables (all starting with var_) are added:
77 # var_group cfg_owning_group if defined otherwise `id -gn`
78 # var_git_ver The version number part from `git version`
79 # var_git_exec_path The result of $cfg_git_bin --exec-dir
80 # var_sh_bin Full path to the posix sh interpreter to use
81 # var_perl_bin Full path to the perl interpreter to use
82 # var_gzip_bin Full path to the gzip executable to use
83 # var_nc_openbsd_bin Full path to the netcat (nc) with -U support
84 # var_have_git_171 Set to 1 if git version >= 1.7.1 otherwise ''
85 # var_have_git_172 Set to 1 if git version >= 1.7.2 otherwise ''
86 # var_have_git_173 Set to 1 if git version >= 1.7.3 otherwise ''
87 # var_have_git_1710 Set to 1 if git version >= 1.7.10 otherwise ''
88 # var_have_git_185 Set to 1 if git version >= 1.8.5 otherwise ''
89 # var_have_git_210 Set to 1 if git version >= 2.1.0 otherwise ''
90 # var_window_memory Value to use for repack --window-memory=
91 # var_big_file_threshold Value to use for core.bigFileThreshold
92 # var_redelta_threshold Recompute deltas if no more than this many objs
93 # var_upload_window If not "", pack.window to use for upload-pack
94 # var_log_window_size Value to use for git-svn --log-window-size=
95 # var_utf8_locale Value to use for a UTF-8 locale if available
96 # var_xargs_r A "-r" if xargs needs it to behave correctly
97 # var_du_exclude Option to exclude PATTERN from du if available
98 # var_du_follow Option to follow command line sym links if available
99 _cfg_vars="$(get_girocco_config_pm_var_list)"
100 eval "$_cfg_vars"
101 printf '%s\n' "$_cfg_vars"
102 printf 'var_group=%s\n' "${cfg_owning_group:-$(id -gn)}"
103 _gver="$("$cfg_git_bin" version 2>/dev/null |
104 LC_ALL=C sed -ne 's/^[^0-9]*\([0-9][0-9]*\(\.[0-9][0-9]*\)*\).*$/\1/p')"
105 printf 'var_git_ver=%s\n' "$_gver"
106 printf 'var_git_exec_path="%s"\n' "$("$cfg_git_bin" --exec-path 2>/dev/null)"
107 printf 'var_sh_bin="%s"\n' "$(_fcp "${cfg_posix_sh_bin:-/bin/sh}")"
108 printf 'var_perl_bin="%s"\n' "$(_fcp "${cfg_perl_bin:-$(unset -f perl; command -v perl)}")"
109 printf 'var_gzip_bin="%s"\n' "$(_fcp "${cfg_gzip_bin:-$(unset -f gzip; command -v gzip)}")"
110 printf 'var_nc_openbsd_bin="%s"\n' "$(_fcp "${cfg_nc_openbsd_bin:-$(unset -f nc; command -v nc)}")"
111 printf 'var_have_git_171=%s\n' "$([ $(vcmp "$_gver" 1.7.1) -ge 0 ] && echo 1)"
112 printf 'var_have_git_172=%s\n' "$([ $(vcmp "$_gver" 1.7.2) -ge 0 ] && echo 1)"
113 printf 'var_have_git_173=%s\n' "$([ $(vcmp "$_gver" 1.7.3) -ge 0 ] && echo 1)"
114 printf 'var_have_git_1710=%s\n' "$([ $(vcmp "$_gver" 1.7.10) -ge 0 ] && echo 1)"
115 printf 'var_have_git_185=%s\n' "$([ $(vcmp "$_gver" 1.8.5) -ge 0 ] && echo 1)"
116 printf 'var_have_git_210=%s\n' "$([ $(vcmp "$_gver" 2.1.0) -ge 0 ] && echo 1)"
117 __girocco_conf="$GIROCCO_CONF"
118 [ -n "$__girocco_conf" ] || __girocco_conf="Girocco::Config"
119 [ -z "$basedir" ] || __girocco_extrainc="-I$basedir"
120 printf "var_window_memory=%s\n" \
121 "$(perl -I@basedir@ $__girocco_extrainc -M$__girocco_conf \
122 -MGirocco::Util -e 'print calc_windowmemory')"
123 printf "var_big_file_threshold=%s\n" \
124 "$(perl -I@basedir@ $__girocco_extrainc -M$__girocco_conf \
125 -MGirocco::Util -e 'print calc_bigfilethreshold')"
126 printf "var_redelta_threshold=%s\n" \
127 "$(perl -I@basedir@ $__girocco_extrainc -M$__girocco_conf \
128 -MGirocco::Util -e 'print calc_redeltathreshold')"
129 if [ -n "$cfg_upload_pack_window" ] && [ "$cfg_upload_pack_window" -ge 2 ] &&
130 [ "$cfg_upload_pack_window" -le 50 ]; then
131 printf "var_upload_window=%s\n" "$cfg_upload_pack_window"
132 else
133 printf "var_upload_window=%s\n" ""
135 printf 'var_log_window_size=%s\n' "${cfg_svn_log_window_size:-250}"
136 # We parse the output of `locale -a` and select a suitable UTF-8 locale.
137 _guess_locale="$(locale -a | LC_ALL=C grep -viE '^(posix|c)(\..*)?$' |
138 LC_ALL=C grep -iE '\.utf-?8$' | LC_ALL=C sed -e 's/\.[Uu][Tt][Ff]-*8$//' |
139 LC_ALL=C sed -e '/en_US/ s/^/0 /; /en_US/ !s/^/1 /' | LC_ALL=C sort |
140 head -n 1 | LC_ALL=C cut -d ' ' -f 2)"
141 [ -z "$_guess_locale" ] || printf 'var_utf8_locale=%s.UTF-8\n' "$_guess_locale"
142 # On some broken platforms running xargs without -r and empty input runs the command
143 printf 'var_xargs_r=%s\n' "$(</dev/null command xargs printf %s -r)"
144 # The disk usage report produces better numbers if du has an exclude option
145 _x0="$(basename "$0")"
146 _x0="${_x0%?}?*"
147 for _duopt in --exclude -I; do
148 if _test="$(du $_duopt 's?lib.s*' $_duopt "$_x0" "$0" 2>/dev/null)" && [ -z "$_test" ]; then
149 printf 'var_du_exclude=%s\n' "$_duopt"
150 break
152 done
153 if _test="$(du -H "$0" 2>/dev/null)" && [ -n "$_test" ]; then
154 printf 'var_du_follow=%s\n' "-H"
155 break
159 # If basedir has been replaced, and shlib_vars.sh exists, get the config
160 # definitions from it rather than running Perl.
161 if [ "@basedir@" = '@'basedir'@' ] || ! [ -r "@basedir@/shlib_vars.sh" ]; then
162 # Import all the variables from Girocco::Config to the local environment,
163 eval "$(get_girocco_config_var_list)"
164 else
165 # Import the variables from shlib_vars.sh which avoids needlessly
166 # running another copy of Perl
167 . "@basedir@/shlib_vars.sh"
170 # git_add_config "some.var=value"
171 # every ' in value must be replaced with the 4-character sequence '\'' before
172 # calling this function or Git will barf. Will not be effective unless running
173 # Git version 1.7.3 or later.
174 git_add_config() {
175 GIT_CONFIG_PARAMETERS="${GIT_CONFIG_PARAMETERS:+$GIT_CONFIG_PARAMETERS }'$1'"
176 export GIT_CONFIG_PARAMETERS
179 # Make sure we have a reproducible environment by using a controlled HOME dir
180 XDG_CONFIG_HOME="$cfg_chroot/var/empty"
181 HOME="$cfg_chroot/etc/girocco"
182 TMPDIR="/tmp"
183 GIT_CONFIG_NOSYSTEM=1
184 GIT_ATTR_NOSYSTEM=1
185 GIT_NO_REPLACE_OBJECTS=1
186 GIT_TERMINAL_PROMPT=0
187 GIT_PAGER="cat"
188 PAGER="cat"
189 GIT_ASKPASS="$cfg_basedir/bin/git-askpass-password"
190 GIT_SVN_NOTTY=1
191 export XDG_CONFIG_HOME
192 export HOME
193 export TMPDIR
194 export GIT_CONFIG_NOSYSTEM
195 export GIT_ATTR_NOSYSTEM
196 export GIT_NO_REPLACE_OBJECTS
197 export GIT_TERMINAL_PROMPT
198 export GIT_PAGER
199 export PAGER
200 export GIT_ASKPASS
201 export GIT_SVN_NOTTY
202 unset GIT_USER_AGENT
203 unset GIT_HTTP_USER_AGENT
204 if [ -n "$defined_cfg_git_client_ua" ]; then
205 GIT_USER_AGENT="$cfg_git_client_ua"
206 export GIT_USER_AGENT
207 GIT_HTTP_USER_AGENT="$cfg_git_client_ua"
208 export GIT_HTTP_USER_AGENT
210 unset GIT_CONFIG_PARAMETERS
211 git_add_config "core.ignoreCase=false"
212 git_add_config "core.pager=cat"
213 if [ -n "$cfg_git_no_mmap" ]; then
214 # Just like compiling with NO_MMAP
215 git_add_config "core.packedGitWindowSize=1m"
216 else
217 # Always use the 32-bit default (32m) even on 64-bit to avoid memory blowout
218 git_add_config "core.packedGitWindowSize=32m"
220 [ -z "$var_big_file_threshold" ] ||
221 git_add_config "core.bigFileThreshold=$var_big_file_threshold"
223 # Make sure any sendmail.pl config is always available
224 unset SENDMAIL_PL_HOST
225 unset SENDMAIL_PL_PORT
226 unset SENDMAIL_PL_NCBIN
227 unset SENDMAIL_PL_NCOPT
228 [ -z "$cfg_sendmail_pl_host" ] || { SENDMAIL_PL_HOST="$cfg_sendmail_pl_host" && export SENDMAIL_PL_HOST; }
229 [ -z "$cfg_sendmail_pl_port" ] || { SENDMAIL_PL_PORT="$cfg_sendmail_pl_port" && export SENDMAIL_PL_PORT; }
230 [ -z "$cfg_sendmail_pl_ncbin" ] || { SENDMAIL_PL_NCBIN="$cfg_sendmail_pl_ncbin" && export SENDMAIL_PL_NCBIN; }
231 [ -z "$cfg_sendmail_pl_ncopt" ] || { SENDMAIL_PL_NCOPT="$cfg_sendmail_pl_ncopt" && export SENDMAIL_PL_NCOPT; }
233 # Set PATH and PYTHON to the values set by Config.pm, if any
234 unset PYTHON
235 [ -z "$cfg_python" ] || { PYTHON="$cfg_python" && export PYTHON; }
236 [ -z "$cfg_path" ] || { orig_path="$PATH" && PATH="$cfg_path" && export PATH; }
238 # Extra GIT variables that generally ought to be cleared, but whose clearing
239 # could potentially interfere with the correct operation of hook scripts so
240 # they are segregated into a separate function for use as appropriate
241 clean_git_env() {
242 unset GIT_ALTERNATE_OBJECT_DIRECTORIES
243 unset GIT_CONFIG
244 unset GIT_DIR
245 unset GIT_GRAFT_FILE
246 unset GIT_INDEX_FILE
247 unset GIT_OBJECT_DIRECTORY
248 unset GIT_NAMESPACE
251 # We cannot use a git() {} or nc_openbsd() {} function to redirect git
252 # and nc_openbsd to the desired executables because when using
253 # "ENV_VAR=xxx func" the various /bin/sh implementations behave in various
254 # different and unexpected ways:
255 # a) treat "ENV_VAR=xxx" like a separate, preceding "export ENV_VAR=xxx"
256 # b) treat "ENV_VAR=xxx" like a separate, prededing "ENV_VAR=xxx"
257 # c) treat "ENV_VAR=xxx" like a temporary setting only while running func
258 # None of these are good. We want a temporary "export ENV_VAR=xxx"
259 # setting only while running func which none of the /bin/sh's do.
261 # Instead we'd like to use an alias that provides the desired behavior without
262 # any of the bad (a), (b) or (c) effects.
264 # However, unfortunately, some of the crazy /bin/sh implementations do not
265 # recognize alias expansions when preceded by variable assignments!
267 # So we are left with git() {} and nc_openbsd() {} functions and in the
268 # case of git() {} we can compensate for (b) and (c) failing to export
269 # but not (a) and (b) persisting the values so the caller will simply
270 # have to beware and explicitly unset any variables that should not persist
271 # beyond the function call itself.
273 git() (
274 [ z"${GIT_DIR+set}" != z"set" ] || export GIT_DIR
275 [ z"${GIT_SSL_NO_VERIFY+set}" != z"set" ] || export GIT_SSL_NO_VERIFY
276 [ z"${GIT_TRACE_PACKET+set}" != z"set" ] || export GIT_TRACE_PACKET
277 [ z"${GIT_USER_AGENT+set}" != z"set" ] || export GIT_USER_AGENT
278 [ z"${GIT_HTTP_USER_AGENT+set}" != z"set" ] || export GIT_HTTP_USER_AGENT
279 exec "$cfg_git_bin" "$@"
282 # Since we do not yet require at least Git 1.8.5 this is a compatibility function
283 # that allows us to use git update-ref --stdin where supported and the slow shell
284 # script where not, but only the "delete" operation is currently supported.
285 git_updateref_stdin() {
286 if [ -n "$var_have_git_185" ]; then
287 git update-ref --stdin
288 else
289 while read -r _op _ref; do
290 case "$_op" in
291 delete)
292 git update-ref -d "$_ref"
295 echo "bad git_updateref_stdin op: $_op" >&2
296 exit 1
298 esac
299 done
303 # see comments for git() -- callers must explicitly export all variables
304 # intended for the commands these functions run before calling them
305 perl() { command "${var_perl_bin:-perl}" "$@"; }
306 gzip() { command "${var_gzip_bin:-gzip}" "$@"; }
308 nc_openbsd() { command "$var_nc_openbsd_bin" "$@"; }
310 list_packs() { command "$cfg_basedir/bin/list_packs" "$@"; }
312 strftime() { command "$cfg_basedir/bin/strftime" "$@"; }
314 # Some platforms' broken xargs runs the command always at least once even if
315 # there's no input unless given a special option. Automatically supply the
316 # option on those platforms by providing an xargs function.
317 xargs() { command xargs $var_xargs_r "$@"; }
319 _addrlist() {
320 _list=
321 for _addr in "$@"; do
322 [ -z "$_list" ] || _list="$_list, "
323 _list="$_list$_addr"
324 done
325 echo "$_list"
328 _sendmail() {
329 _mailer="${cfg_sendmail_bin:-/usr/sbin/sendmail}"
330 if [ -n "$cfg_sender" ]; then
331 "$_mailer" -i -f "$cfg_sender" "$@"
332 else
333 "$_mailer" -i "$@"
337 # First argument is an id WITHOUT surrounding '<' and '>' to use in a
338 # "References:" header. It may be "" to suppress the "References" header.
339 # Following arguments are just like mail function
340 mailref() {
341 _references=
342 if [ $# -ge 1 ]; then
343 _references="$1"
344 shift
346 _subject=
347 if [ "$1" = "-s" ]; then
348 shift
349 _subject="$1"
350 shift
353 echo "From: \"$cfg_name\" ($cfg_title) <$cfg_admin>"
354 echo "To: $(_addrlist "$@")"
355 [ -z "$_subject" ] || echo "Subject: $_subject"
356 echo "MIME-Version: 1.0"
357 echo "Content-Type: text/plain; charset=UTF-8"
358 echo "Content-Transfer-Encoding: 8bit"
359 [ -z "$_references" ] || echo "References: <$_references>"
360 [ -n "$cfg_suppress_x_girocco" ] || echo "X-Girocco: $cfg_gitweburl"
361 echo "Auto-Submitted: auto-generated"
362 echo ""
364 } | _sendmail "$@"
367 # Usage: mail [-s <subject>] <addr> [<addr>...]
368 mail() {
369 mailref "" "$@"
372 # bang CMD... will execute the command with well-defined failure mode;
373 # set bang_action to string of the failed action ('clone', 'update', ...);
374 # re-define the bang_trap() function to do custom cleanup before bailing out
375 bang() {
376 bang_active=1
377 bang_cmd="$*"
378 bang_errcode=0
379 if [ -n "$show_progress" ]; then
380 exec 3>&1
381 read -r bang_errcode <<-EOT || :
383 exec 4>&3 3>&1 1>&4 4>&-
384 { "$@" 3>&- || echo $? >&3; } 2>&1 | tee -i -a "$bang_log"
387 exec 3>&-
388 if [ -z "$bang_errcode" ] || [ "$bang_errcode" = "0" ]; then
389 # All right. Cool.
390 bang_active=
391 bang_cmd=
392 return;
394 else
395 if "$@" >>"$bang_log" 2>&1; then
396 # All right. Cool.
397 bang_active=
398 bang_cmd=
399 return;
400 else
401 bang_errcode="$?"
404 bang_failed
407 bang_failed() {
408 bang_active=
409 unset GIT_DIR
410 >.banged
411 cat "$bang_log" >.banglog
412 echo "" >>.banglog
413 echo "$bang_cmd failed with error code $bang_errcode" >>.banglog
414 if [ -n "$show_progress" ]; then
415 echo ""
416 echo "$bang_cmd failed with error code $bang_errcode"
418 if [ -e .bangagain ]; then
419 git config --remove-section girocco.bang 2>/dev/null || :
420 rm -f .bangagain
422 bangcount="$(git config --int girocco.bang.count 2>/dev/null)" || :
423 bangcount=$(( ${bangcount:-0} + 1 ))
424 git config --int girocco.bang.count $bangcount
425 if [ $bangcount -eq 1 ]; then
426 git config girocco.bang.firstfail "$(TZ=UTC date "+%Y-%m-%d %T UTC")"
428 if [ $bangcount -ge $cfg_min_mirror_failure_message_count ] &&
429 [ "$(git config --bool girocco.bang.messagesent 2>/dev/null || :)" != "true" ] &&
430 ! check_interval "girocco.bang.firstfail" $cfg_min_mirror_failure_message_interval; then
431 bangmailok="$(git config --bool gitweb.statusupdates 2>/dev/null || echo true)"
432 bangaddrs=
433 [ "$bangmailok" = "false" ] || [ -z "$mail" ] || bangaddrs="$mail"
434 [ -z "$cfg_admincc" ] || [ "$cfg_admincc" = "0" ] || [ -z "$cfg_admin" ] ||
435 if [ -z "$bangaddrs" ]; then bangaddrs="$cfg_admin"; else bangaddrs="$bangaddrs,$cfg_admin"; fi
436 rsubj=
437 [ $bangcount -le 1 ] || rsubj=" repeatedly"
438 [ -z "$bangaddrs" ] ||
440 echo "$bang_cmd failed with error code $bang_errcode"
441 echo ""
442 rsubj=
443 if [ $bangcount -gt 1 ]; then
444 echo "$bangcount consecutive update failures have occurred since $(config_get girocco.bang.firstfail)"
445 echo ""
447 echo "you will not receive any more notifications until recovery"
448 echo "this status message may be disabled on the project admin page"
449 echo ""
450 echo "Log follows:"
451 echo ""
452 cat "$bang_log"
453 } | mailref "update@$cfg_gitweburl/$proj.git" -s "[$cfg_name] $proj $bang_action failed$rsubj" "$bangaddrs"
454 git config --bool girocco.bang.messagesent true
456 bangthrottle=
457 [ $bangcount -lt 15 ] ||
458 check_interval "girocco.bang.firstfail" $(( $cfg_min_mirror_interval * 3 / 2 )) ||
459 bangthrottle=1
460 bang_trap $bangthrottle
461 [ -n "$bang_errcode" ] && [ "$bang_errcode" != "0" ] || bang_errcode=1
462 exit $bang_errcode
465 # bang_eval CMD... will evaluate the command with well-defined failure mode;
466 # Identical to bang CMD... except the command is eval'd instead of executed.
467 bang_eval() {
468 bang eval "$*"
471 # Default bang settings:
472 bang_setup() {
473 bang_active=
474 bang_action="lame_programmer"
475 bang_trap() { :; }
476 bang_tmpdir="${TMPDIR:-/tmp}"
477 bang_tmpdir="${bang_tmpdir%/}"
478 bang_log="$(mktemp "${bang_tmpdir:-/tmp}/repomgr-XXXXXX")"
479 is_git_dir . || {
480 echo "bang_setup called with current directory not a git directory" >&2
481 exit 1
483 trap 'rm -f "$bang_log"' EXIT
484 trap '[ -z "$bang_active" ] || { bang_errcode=130; bang_failed; }; exit 130' INT
485 trap '[ -z "$bang_active" ] || { bang_errcode=143; bang_failed; }; exit 143' TERM
488 # Remove banged status
489 bang_reset() {
490 rm -f .banged .bangagain .banglog
491 git config --remove-section girocco.bang 2>/dev/null || :
494 # Check to see if banged status
495 is_banged() {
496 [ -e .banged ]
499 # Check to see if banged message was sent
500 was_banged_message_sent() {
501 [ "$(git config --bool girocco.bang.messagesent 2>/dev/null || :)" = "true" ]
504 # Progress report - if show_progress is set, shows the given message.
505 progress() {
506 [ -z "$show_progress" ] || echo "$*"
509 # Project config accessors; must be run in project directory
510 config_get() {
511 case "$1" in
512 *.*)
513 git config "$1";;
515 git config "gitweb.$1";;
516 esac
519 config_set() {
520 git config "gitweb.$1" "$2" && chgrp $var_group config && chmod g+w config
523 config_set_raw() {
524 git config "$1" "$2" && chgrp $var_group config && chmod g+w config
527 config_get_date_seconds() {
528 _dt="$(config_get "$1")" || :
529 [ -n "$_dt" ] || return 1
530 _ds="$(perl -I@basedir@ -MGirocco::Util -e "print parse_any_date('$_dt')")"
531 [ -n "$_ds" ] || return 1
532 echo "$_ds"
535 # Tool for checking whether given number of seconds has not passed yet
536 check_interval() {
537 os="$(config_get_date_seconds "$1")" || return 1
538 ns="$(date +%s)"
539 [ $ns -lt $(($os+$2)) ]
542 # Check if we are running with effective root permissions
543 is_root() {
544 [ "$(id -u 2>/dev/null)" = "0" ]
547 # Check to see if the single argument is a Git directory
548 is_git_dir() {
549 # Just like Git's test except we ignore GIT_OBJECT_DIRECTORY
550 # And we are slightly more picky (must be refs/.+ not refs/.*)
551 [ -d "$1/objects" ] && [ -x "$1/objects" ] || return 1
552 [ -d "$1/refs" ] && [ -x "$1/refs" ] || return 1
553 if [ -L "$1/HEAD" ]; then
554 _hr="$(readlink "$1/HEAD")"
555 case "$_hr" in "refs/"?*) :;; *) return 1;; esac
557 [ -f "$1/HEAD" ] && [ -r "$1/HEAD" ] || return 1
558 read -r _hr <"$1/HEAD" || return 1
559 case "$_hr" in
560 $octet20 | ref:refs/?*)
561 return 0;;
562 ref:*)
563 _hr="${_hr##ref:*[ $tab]}"
564 case "$_hr" in "refs/"?*) return 0;; esac
565 esac
566 return 1
569 # List all Git repositories, with given prefix if specified, one-per-line
570 # All project names starting with _ are always excluded from the result
571 get_repo_list() {
572 if [ -n "$1" ]; then
573 LC_ALL=C cut -d : -f 1,3 "$cfg_chroot"/etc/group | LC_ALL=C grep "^$1"
574 else
575 LC_ALL=C cut -d : -f 1,3 "$cfg_chroot"/etc/group
576 fi | while IFS=: read name id; do
577 [ $id -lt 65536 ] || case "$name" in _*) :;; ?*) echo "$name"; esac
578 done
581 # Return success if the given project name has any forks
582 has_forks() {
583 _prj="${1%.git}"
584 [ -n "$_prj" ] || return 1
585 [ -d "$cfg_reporoot/$_prj" ] || return 1
586 is_git_dir "$cfg_reporoot/$_prj.git" || return 1
587 test $(get_repo_list "$_prj/[^/:][^/:]*:" | LC_ALL=C wc -l) -gt 0
590 # returns empty string and error for empty string otherwise one of
591 # m => normal Git mirror
592 # s => mirror from svn source
593 # d => mirror from darcs source
594 # b => mirror from bzr source
595 # h => mirror from hg source
596 # w => mirror from mediawiki source
597 # f => mirror from other fast-import source
598 # note that if the string is non-empty and none of s, d, b or h match the
599 # return will always be type m regardless of whether it's a valid Git URL
600 get_url_mirror_type() {
601 case "$1" in
603 return 1
605 svn://* | svn+http://* | svn+https://* | svn+file://* | svn+ssh://*)
606 echo 's'
608 darcs://*)
609 echo 'd'
611 bzr://*)
612 echo 'b'
614 hg+http://* | hg+https://* | hg+file://* | hg+ssh://* | hg::*)
615 echo 'h'
617 mediawiki::*)
618 echo 'w'
621 echo 'm'
623 esac
624 return 0
627 # returns false for empty string
628 # returns true if the passed in url is a mirror using git fast-import
629 is_gfi_mirror_url() {
630 [ -n "$1" ] || return 1
631 case "$(get_url_mirror_type "$1" 2>/dev/null || :)" in
632 d|b|h|w|f)
633 # darcs, bzr, hg and mediawiki mirrors use git fast-import
634 # and so do generic "f" fast-import mirrors
635 return 0
638 # Don't think git-svn currently uses git fast-import
639 # And Git mirrors certainly do not
640 return 1
642 esac
643 # assume it does not use git fast-import
644 return 1
647 # returns false for empty string
648 # returns true if the passed in url is a mirror using git-svn
649 is_svn_mirror_url() {
650 [ -n "$1" ] || return 1
651 [ "$(get_url_mirror_type "$1" 2>/dev/null || :)" = "s" ]
654 # returns mirror url for gitweb.baseurl of git directory
655 # (GIT_DIR) passed in as the argument (which defaults to "." if omitted)
656 # will fail if the directory does not have .nofetch and gitweb.baseurl
657 # comes back empty -- otherwise .nofetch directories succeed with a "" return
658 # automatically strips any leading "disabled " prefix before returning result
659 get_mirror_url() {
660 _gitdir="${1:-.}"
661 # always return empty for non-mirrors
662 ! [ -e "$_gitdir/.nofetch" ] || return 0
663 _url="$(GIT_DIR="$_gitdir" config_get baseurl 2>/dev/null)" || :
664 _url="${_url##* }"
665 [ -n "$_url" ] || return 1
666 printf '%s\n' "$_url"
667 return 0
670 # returns get_url_mirror_type for gitweb.baseurl of git directory
671 # (GIT_DIR) passed in as the argument (which defaults to "." if omitted)
672 # will fail if the directory does not have .nofetch and gitweb.baseurl
673 # comes back empty -- otherwise .nofetch directories succeed with a "" return
674 # automatically strips any leading "disabled " prefix before testing
675 get_mirror_type() {
676 _url="$(get_mirror_url "$@")" || return 1
677 [ -n "$_url" ] || return 0
678 get_url_mirror_type "$_url"
681 # returns true if the passed in git dir (defaults to ".") is a mirror using git fast-import
682 is_gfi_mirror() {
683 _url="$(get_mirror_url "$@")" || return 1
684 is_gfi_mirror_url "$_url"
687 # returns true if the passed in git dir (defaults to ".") is a mirror using git-svn
688 is_svn_mirror() {
689 _url="$(get_mirror_url "$@")" || return 1
690 is_svn_mirror_url "$_url"
693 # current directory must already be set to Git repository
694 # if girocco.headok is already true succeeds without doing anything
695 # if rev-parse --verify HEAD succeeds sets headok=true and succeeds
696 # otherwise tries to set HEAD to a symbolic ref to refs/heads/master
697 # then refs/heads/trunk and finally the first top-level head from
698 # refs/heads/* (i.e. only two slashes in the name) and finally any
699 # existing refs/heads. The first one to succeed wins and sets headok=true
700 # and then a successful exit. Otherwise headok is left unset with a failure exit
701 # We use the girocco.headok flag to make sure we only force a valid HEAD symref
702 # when the repository is being set up -- if the HEAD is later deleted (through
703 # a push or fetch --prune) that's no longer our responsibility to fix
704 check_and_set_head() {
705 [ "$(git config --bool girocco.headok 2>/dev/null || :)" != "true" ] || return 0
706 if git rev-parse --verify --quiet HEAD >/dev/null; then
707 git config --bool girocco.headok true
708 return 0
710 for _hr in refs/heads/master refs/heads/trunk; do
711 if git rev-parse --verify --quiet "$_hr"; then
712 _update_head_symref "$_hr"
713 return 0
715 done
716 git for-each-ref --format="%(refname)" refs/heads 2>/dev/null |
717 while read -r _hr; do
718 case "${_hr#refs/heads/}" in */*) :;; *)
719 _update_head_symref "$_hr"
720 exit 1 # exit subshell created by "|"
721 esac
722 done || return 0
723 _hr="$(git for-each-ref --format="%(refname)" refs/heads 2>/dev/null | head -n 1)" || :
724 if [ -n "$_hr" ]; then
725 _update_head_symref "$_hr"
726 return 0
728 return 1
730 _update_head_symref() {
731 git symbolic-ref HEAD "$1"
732 git config --bool girocco.headok true
733 ! [ -d htmlcache ] || { >htmlcache/changed; } 2>/dev/null || :
736 # A well-known UTF-8 locale is required for some of the fast-import providers
737 # in order to avoid mangling characters. Ideally we could use "POSIX.UTF-8"
738 # but that is not reliably UTF-8 but rather usually US-ASCII.
739 # We parse the output of `locale -a` and select a suitable UTF-8 locale at
740 # install time and store that in $var_utf8_locale if one is found.
741 # If we cannot find one in the `locale -a` output then we just use a well-known
742 # UTF-8 locale and hope for the best. We set LC_ALL to our choice and export
743 # it. We only set this temporarily when running the fast-import providers.
744 set_utf8_locale() {
745 LC_ALL="${var_utf8_locale:-en_US.UTF-8}"
746 export LC_ALL
749 # hg-fast-export | git fast-import with error handling in current directory GIT_DIR
750 git_hg_fetch() (
751 set_utf8_locale
752 _python="${PYTHON:-python}"
753 rm -f hg2git-marks.old hg2git-marks.new
754 if [ -f hg2git-marks ] && [ -s hg2git-marks ]; then
755 LC_ALL=C sed 's/^:\([^ ][^ ]*\) \([^ ][^ ]*\)$/\2 \1/' <hg2git-marks | {
756 if [ -n "$var_have_git_185" ]; then
757 git cat-file --batch-check=':%(rest) %(objectname)'
758 else
759 LC_ALL=C sed 's/^\([^ ][^ ]*\) \([^ ][^ ]*\)$/:\2 \1/'
761 } | LC_ALL=C sed '/ missing$/d' >hg2git-marks.old
762 if [ -n "$var_have_git_171" ] &&
763 git rev-parse --quiet --verify refs/notes/hg >/dev/null; then
764 if [ -z "$var_have_git_185" ] ||
765 ! LC_ALL=C cmp -s hg2git-marks hg2git-marks.old; then
766 _nm='hg-fast-export'
767 GIT_AUTHOR_NAME="$_nm"
768 GIT_COMMITTER_NAME="$_nm"
769 GIT_AUTHOR_EMAIL="$_nm"
770 GIT_COMMITTER_EMAIL="$_nm"
771 export GIT_AUTHOR_NAME
772 export GIT_COMMITTER_NAME
773 export GIT_AUTHOR_EMAIL
774 export GIT_COMMITTER_EMAIL
775 git notes --ref=refs/notes/hg prune
776 unset GIT_AUTHOR_NAME
777 unset GIT_COMMITTER_NAME
778 unset GIT_AUTHOR_EMAIL
779 unset GIT_COMMITTER_EMAIL
782 else
783 >hg2git-marks.old
785 _err1=
786 _err2=
787 exec 3>&1
788 { read -r _err1 || :; read -r _err2 || :; } <<-EOT
790 exec 4>&3 3>&1 1>&4 4>&-
792 _e1=0
793 _af="$(git config hg.authorsfile)" || :
794 _cmd='GIT_DIR="$(pwd)" "$_python" "$cfg_basedir/bin/hg-fast-export.py" \
795 --repo "$(pwd)/repo.hg" \
796 --marks "$(pwd)/hg2git-marks.old" \
797 --mapping "$(pwd)/hg2git-mapping" \
798 --heads "$(pwd)/hg2git-heads" \
799 --status "$(pwd)/hg2git-state" \
800 -U unknown --force --flatten --hg-hash'
801 [ -z "$_af" ] || _cmd="$_cmd"' --authors "$_af"'
802 eval "$_cmd" 3>&- || _e1=$?
803 echo $_e1 >&3
806 _e2=0
807 git fast-import \
808 --import-marks="$(pwd)/hg2git-marks.old" \
809 --export-marks="$(pwd)/hg2git-marks.new" \
810 --export-pack-edges="$(pwd)/gfi-packs" \
811 --force 3>&- || _e2=$?
812 echo $_e2 >&3
816 exec 3>&-
817 [ "$_err1" = 0 ] && [ "$_err2" = 0 ] || return 1
818 mv -f hg2git-marks.new hg2git-marks
819 rm -f hg2git-marks.old
820 git for-each-ref --format='%(refname) %(objectname)' refs/heads |
821 LC_ALL=C sed -e 's,^refs/heads/,:,' >hg2git-heads