set -ue
function cehgit
{
cehgit_loadconfig
case "$0" in
*/cehgit|*/.cehgit)
case "${1:-}" in
help|-h|--help|"")
shift
show_help
;;
init)
shift
cehgit_init "$@"
;;
update)
shift
cehgit_init --update
cehgit_install_action --update
;;
install-action)
shift
cehgit_install_action "$@"
;;
update-actions)
shift
cehgit_install_action --update
;;
list-actions)
shift
cehgit_list_actions
;;
available-actions)
shift
cehgit_available_actions
;;
install-hook)
shift
cehgit_install_hook "$@"
;;
remove-hook)
shift
cehgit_remove_hook "$@"
;;
list-hooks)
shift
cehgit_list_hooks
;;
log)
shift
cehgit_log
;;
clean)
shift
cehgit_clean
;;
run)
declare -gr CEHGIT_HOOK="$RUN_HOOK"
BACKGROUNDING="false"
VERBOSITY_LEVEL=$((VERBOSITY_LEVEL+1))
cehgit_runner "$@"
;;
*)
die "unknown sub-command"
esac
;;
*.git/hooks/*)
declare -gr CEHGIT_HOOK="${0##*/}"
cehgit_runner "$@"
;;
*)
die "invalid invocation $0"
;;
esac
}
function cehgit_loadconfig
{
declare -grx ACTIONS_DIR=".cehgit.d"
declare -gx KEEP_TESTS=5 declare -gx NICE_LEVEL=1 declare -gx TIMEOUT_SECS=10 declare -gx MEMORY_KB=16777216 declare -gx TEST_PREFIX=".test-" declare -gx TEST_LOG="test.log" declare -gx VERBOSITY_LEVEL="${VERBOSITY_LEVEL:-2}" declare -gx BACKGROUNDING="true" declare -gx RUN_HOOK="pre-commit" declare -gx CEHGIT_HOOKS=(pre-commit pre-merge-commit prepare-commit-msg commit-msg)
load_existing "~/config/cehgit.conf" "~/.cehgit.conf" ".git/cehgit.conf" ".cehgit.conf"
[[ $KEEP_TESTS -ge 2 ]] || die "KEEP_TESTS must be larger or equal to two"
declare -gx WORKTREE_DIR="$(pwd)" declare -gx LAST_TEST_DIR declare -gx TEST_DIR }
function cehgit_init
{
[[ -d ".git" ]] || die "not a git repository"
case "${1:-}" in
"-f"|"--force"|"--update") FORCE=true
shift
;;
esac
if [[ ! -f ".cehgit" ]]; then
info "installing .cehgit"
cp "$0" "./.cehgit"
mkdir -p "$ACTIONS_DIR"
elif [[ "${FORCE:-}" == true ]]; then
info "updating .cehgit"
cp -f "$0" "./.cehgit"
else
info ".cehgit already installed"
fi
}
function cehgit_install_action
{
[[ -d "$ACTIONS_DIR" ]] || die "cehgit not initialized"
local actions_origin
actions_origin=$(first_dir "~/.local/share/cehgit/actions" "/usr/share/cehgit/actions")
[[ -n "$actions_origin" ]] || die "no origin actions dir found"
while [[ $# -ne 0 ]]; do
case "${1:-}" in
"-f"|"--force") FORCE="-f -u"
shift
;;
"--all") debug "installing all actions"
cp -v ${FORCE:--n} "$actions_origin/"[0-9]* "$actions_origin/mod-"* "$ACTIONS_DIR/"
return 0
;;
"--update") debug "updating all actions"
cp -v -u "$actions_origin/"[0-9]* "$actions_origin/mod-"* "$ACTIONS_DIR/"
return 0
;;
*)
break
;;
esac
done
for action in "$@"; do
cp -v ${FORCE:--n} "$actions_origin/"$action "$ACTIONS_DIR/"
done
}
function cehgit_available_actions
{
describe_actions "$(first_dir "~/.local/share/cehgit/actions" "/usr/share/cehgit/actions" || die "no origin actions dir found" )"
}
function cehgit_list_actions
{
[[ -d "$ACTIONS_DIR" ]] || die "cehgit not initialized"
describe_actions "$ACTIONS_DIR"
}
function describe_actions
{
[[ -d "$1" ]] || die "$1 is not a directory"
find "$1/" -maxdepth 1 -type f -name '[0-9]*' -or -name 'mod-*' | sort |
while read -r action; do
echo "${action##*/}:"
awk '/^## ?/{sub(/^## ?/," "); print}' "$action"
echo
done
}
function cehgit_install_hook
{
[[ -x ".cehgit" ]] || die "cehgit not initialized"
while [[ $# -ne 0 ]]; do
case "${1:-}" in
"-f"|"--force") FORCE=true
shift
;;
"--all") for hook in "${CEHGIT_HOOKS[@]}"; do
debug "installing hook $hook"
ln ${FORCE:+-f} -s '../../.cehgit' ".git/hooks/$hook" || error "installing hook $hook failed"
done
return 0
;;
*) rc=0
for hook in "$@"; do
debug "installing hook $hook"
ln ${FORCE:+-f} -s '../../.cehgit' ".git/hooks/$hook" 2>/dev/null ||
{
error "installing hook $hook failed"
rc=1
}
done
return $rc
esac
done
}
function cehgit_remove_hook
{
for hook in "$@"; do
if [[ $(readlink ".git/hooks/$hook") = ../../.cehgit ]]; then
rm ".git/hooks/$hook"
debug "removed hook $hook"
else
error "$hook: not a cehgit controlled hook"
fi
done
}
function cehgit_list_hooks
{
find .git/hooks/ -type l -lname '../../.cehgit' -printf "%f\n"
}
function cehgit_clean
{
[[ -n "$TEST_PREFIX" ]] || die "TEST_PREFIX not set"
find . -name "${TEST_PREFIX}*" -type d -exec rm -r {} +
}
function first_dir {
for dir in "$@"; do
dir="${dir/#~\//$HOME/}"
trace "trying $dir"
if [[ -d "$dir" ]]; then
echo "$dir"
return 0
fi
done
return 1
}
function load_existing {
for file in "$@"; do
file="${file/#~\//$HOME/}"
[[ -f "$file" ]] && {
debug "$file"
source "$file"
}
done
return 0
}
function cehgit_runner
{
declare -gx TREE_HASH
TREE_HASH="$(git write-tree)"
readonly TREE_HASH
find . -name "${TEST_PREFIX}*" -type d | sort -n | head -n -$((KEEP_TESTS-1)) | xargs -r rm -r
LAST_TEST_DIR=$(find . -name "${TEST_PREFIX}*" -type d | sort -rn | head -n 1)
LAST_TEST_DIR="${LAST_TEST_DIR:+$WORKTREE_DIR/${LAST_TEST_DIR#./}}"
debug "LAST_TEST_DIR = $LAST_TEST_DIR"
TEST_DIR=$(find . -type d -name ".test-*-$TREE_HASH" | tail -1)
[[ -z "$TEST_DIR" ]] && TEST_DIR="${TEST_PREFIX}$(awk 'BEGIN {srand(); print srand()}')-$TREE_HASH"
debug "TEST_DIR = $TEST_DIR"
git archive "$TREE_HASH" --prefix="$TEST_DIR/" | tar xf -
ln -sf ../.git "$TEST_DIR/"
cd "$TEST_DIR"
lock_wait .cehgit.lock
state_init .cehgit.log
for ACTION in "$WORKTREE_DIR/$ACTIONS_DIR/"[0-9]* ; do
[[ -f "$ACTION" ]] || continue
ACTION="${ACTION##*/}"
local state="$(state_cached)"
if [[ -z "$state" ]]; then
debug "$ACTION"
if ! source "$WORKTREE_DIR/$ACTIONS_DIR/$ACTION" "$@"; then
lock_remove .cehgit.lock
exit 1
fi
elif [[ "$state" == fail ]]; then
debug "$ACTION cached $state"
exit 1
fi
done |& tee -a "$TEST_LOG" || true
[[ ${PIPESTATUS[0]} != 0 ]] && {
error "action failed"
lock_remove .cehgit.lock
exit 1
}
if state_match _ '*' background; then
info "schedule background jobs"
declare -a BACKGROUND_ACTIONS
mapfile -t BACKGROUND_ACTIONS < <(state_list_actions background)
trace "bg: ${BACKGROUND_ACTIONS[*]}"
(
lock_wait .cehgit.bg.lock
state_init .cehgit.bg.log
touch "$TEST_LOG.bg"
declare -rgx CEHGIT_BACKGROUND=true
for ACTION in "${BACKGROUND_ACTIONS[@]}"; do
ACTION="${ACTION##*/}"
local state="$(state_cached)"
if [[ -z "$state" ]]; then
debug "bg running $ACTION"
source "$WORKTREE_DIR/$ACTIONS_DIR/$ACTION" "$@" || true
[[ "$(state_cached)" =~ ok|fail ]] || {
lock_remove .cehgit.bg.lock
die "background_schedule: background job $ACTION must result in ok or fail"
}
elif [[ "$state" == fail ]]; then
debug "bg $ACTION cached $state"
lock_remove .cehgit.bg.lock
exit 1
fi
done &> >(cat >>"$TEST_LOG.bg")
lock_remove .cehgit.bg.lock
) &
else
debug "no background jobs"
fi
debug "forground runner complete"
lock_remove .cehgit.lock
}
function cehgit_log
{
LAST_TEST_DIR=$(find . -name "${TEST_PREFIX}*" -type d | sort -rn | head -n 1)
[[ -z "$LAST_TEST_DIR" ]] && die "no last test dir found"
less -R "$LAST_TEST_DIR/$TEST_LOG"
}
function state_init {
declare -gx STATE_FILE="$1"
declare -gx STATE_SEQ
STATE_SEQ=$(awk '{max = ($1 > max) ? $1 : max} END {print max + 1}' "$STATE_FILE" 2>/dev/null || echo 0)
trace "STATE_FILE = $STATE_FILE, STATE_SEQ = $STATE_SEQ"
}
function state_cached {
awk '/^[0-9]+ '"$ACTION"' (ok|fail)/{print $3; exit 0;}' "$STATE_FILE" 2>/dev/null
}
function state_log {
trace "$STATE_SEQ $ACTION $1 >> $STATE_FILE"
echo "$STATE_SEQ $ACTION $1" >>"$STATE_FILE"
}
function state_match {
local seq="$1"
local action="${2##*/}"
local state="$3"
[[ "$seq" = "_" ]] && seq="$STATE_SEQ"
[[ "$action" = "_" ]] && action="$ACTION"
[[ "$state" = "_" ]] && state="ok|fail"
[[ "$seq" = "*" ]] && seq="[0-9]+"
[[ "$action" = "*" ]] && action="\\S+"
[[ "$state" = "*" ]] && state="\\S+"
awk "BEGIN {rc=1} /^$seq $action $state\$/{rc=0} END {exit rc}" "$STATE_FILE"
}
function state_list_actions {
awk "/^$STATE_SEQ \\S+ $1/{print \$2}" "$STATE_FILE"
}
function lock_wait {
echo $BASHPID >>"$1"
local lockpid
while { lockpid=$(head -1 "$1"); [[ $lockpid != "$BASHPID" ]]; }; do
trace "wait: $lockpid to complete"
while kill -0 "$lockpid" 2>/dev/null ; do
wait "$lockpid" &>/dev/null || sleep 0.1
done
echo $BASHPID >"$1"
done
trace "locked: $1"
}
function lock_remove {
local lock=$(head -1 "$1" 2>/dev/null)
if [[ "$lock" = "$BASHPID" ]]; then
trace "unlock $1"
rm -f "$1"
return 0
else
error "$1 is not ours"
return 1
fi
}
function run_test {
while [[ $# -gt 0 ]]; do
case "${1:-}" in
-n|--nice)
local NICE_LEVEL="$2"
shift 2
;;
-m|--memory)
local MEMORY_KB="$2"
shift 2
;;
-t|--timeout)
local TIMEOUT_SECS="$2"
shift 2
;;
*)
break
;;
esac
done
info "run_test $*"
if (
renice -n "$NICE_LEVEL" -p $BASHPID >/dev/null
ulimit -S -v "$MEMORY_KB" -t "$TIMEOUT_SECS"
"$@"
); then
state_log ok
return 0
else
state_log fail
return 1
fi
}
function background_schedule {
if [[ "$BACKGROUNDING" != "true" ]] || [[ -n "${CEHGIT_BACKGROUND:-}" ]]; then
trace "execute $ACTION"
return 1
else
if state_match _ _ background; then
info "already scheduled $ACTION"
else
info "schedule $ACTION"
state_log background
fi
fi
}
function background_wait {
if [[ "$BACKGROUNDING" != "true" ]]; then
trace "backgrounding disabled"
return 1
fi
if [[ -n "${CEHGIT_BACKGROUND:-}" ]]; then
trace "running in background $ACTION"
return 1
else
trace "running in foreground $ACTION"
state_match '*' _ background || die "background_result: no background action scheduled"
[[ -n "$(state_cached)" ]] && return 0
if [[ -f .cehgit.bg.log ]]; then
lock_wait .cehgit.bg.lock
trace "collect the background logs"
cat .cehgit.bg.log >>.cehgit.log
cat "$TEST_LOG.bg" >>"$TEST_LOG"
rm .cehgit.bg.log
rm "$TEST_LOG.bg"
lock_remove .cehgit.bg.lock
fi
return 0
fi
}
function background_result {
local state
state="$(state_cached)"
trace "$ACTION $state"
if [[ "$state" = "fail" ]]; then
echo 1
else
echo 0
fi
}
function show_help
{
less <<EOF
cehgit -- cehtehs personal git assistant
ABOUT
cehgit is a frontend for githooks that runs bash scripts (actions) in sequence. This acts
much like a CI but for your local git repository. Unlike some other 'pre-commit' solutions
it will not alter your worktree by stashing changes for the test but run tests in dedicated
directories which are kept around for later inspection and improving test performance.
cehgit caches the state of the last run tests and reuses the test directories when the git
tree did not change. This allows for incremental testing and faster turnaround times.
It schedule tests to run in background. This means tests may run while you type a commit
message.
When you read this because you seen '.cehgit' used in a repository then you may look at
INITIAL INSTALLATION below.
USAGE
cehgit [-h|--help|help]
show this help
cehgit init [-f|--force|--update]
initialize or update the local '.cehgit'
cehgit install-action [-f|--force] [--all|actionglob..]
install actions that are shipped with cehgit
cehgit update-actions
update all actions
cehgit update
update the local '.cehgit', and all installed actions.
cehgit available-actions
list actions that are shipped with cehgit
cehgit list-actions
list actions that are active in the current repository
cehgit install-hook [-f|--force] [--all|hooks..]
install a githook to use cehgit
cehgit remove-hook [--all|hooks..]
delete cehgit controlled githooks
cehgit list-hooks
list all githooks that point to cehgit
cehgit clean
remove all test directories
cehgit log
show the log of the last test run
./.cehgit run
manual run, behaves as from a 'RUN_HOOK' [pre-commit] hook with
BACKGROUNDING=false and VERBOSITY_LEVEL+1
./.cehgit [..]
same as 'cehgit' above but calling the repo local version
./.git/hooks/* [OPTIONS..]
invoking git hooks manually
SETUP
To use cehgit in a git repository it has first to be initialized with 'cehgit init'. This
copies itself to a repository local '.cehgit' and creates the '.cehgit.d/'
directory. 'cehgit init --upgrade' will upgrade an already initialized version.
Then the desired actions have to be written or installed. 'cehgit install-action --all' will
copy all actions shipped with cehgit to '.cehgit.d/'. This should always be safe but may
include more than one may want and implement some opinionated workflow. The installed
actions are meant to be customized to personal preferences.
cehgit puts tests in sub-directories starting with '.test-*'. This pattern should be added
to '.gitignore'.
'.cehgit', '.cehgit.d/*' and '.cehgit.conf' are meant to be commited into git and
versioned.
Once this is set up one should 'cehgit install-hooks [--all]' to setup the desired hooks
locally. Note that installed hooks are not under version control and every checkout of
the repository has to install them manually again.
This completes the setup, cehgit should now by called whenever git invokes a hook.
HOW CEHGIT WORKS
Cehgit is implemented in bash the test actions are sourced in sorted order. Bash was chosen
because of it's wide availability and more addvanced features than standard shells. We rely
on some bashisms. To make shell programming a little more safe it calls 'set -ue' which
ensures that variables are declared before used and exits on the first failure.
Test are run in locally created test directories, the worktree itself is not
altered/stashed. This test directories are populated from the currently staged files in the
working tree. The '\$TEST_DIR/.git/' directory is symlinked to the original '../.git'.
Test directries are reused when they orginate from the same git tree (hash), cehgit
deliberately does not start from a clean/fresh directory to take advantage of incremental
compilation and artifacts from former runs to speed tests up. All actions on a tree are
logged and this log is used to query cached results.
It keeps the last KEEP_TESTS around and removes excess ones.
When invoked as githook a test directory is created or reused and entered. Then all actions
in ACTIONS_DIR are sourced in sorted order. Actions determine by API calls if they should
execute, schedule to background or exit early.
Modules starting with 'mod-' in ACTIONS_DIR can be used to extend the
API. actions use 'require' to load.
API calls with also log the progress, ok/fail states will be reused in subsequent runs.
Many function results are memoized with the 'memo' helper function.
The test directories left behind can be inspected at later time. There will be a 'test.log'
where the stdout of all actions that run is collected.
To debug cehgit execution itself one can set VERBOSITY_LEVEL to a number up to
5 (0=none, 1=error, 2=notice, 3=info, 4=debug, 5=trace)
BACKGROUND DETAILS
Schedule actions to background will finish the currently running hook early before
this background actions are completed. Background actions will be scheduled after all
foreground actions completed successful.
Background actions should finish with a conclusive ok or fail. Using 'run_test' will take
care of that.
Example Action:
# background_schedule will succeed when this action is scheduled and fail when it is already
# scheduled and should be executed. We '&& return 0' to exit when scheduled and fall through on fail
background_schedule && return 0
# To retrieve the result of the background action use background_wait and background_result
# this falls through when no background action was scheduled
background_wait && return \$(background_result)
# eventually run the actual code
run_test make
cehgit utilizes the pre-commit hook to schedule expensive tests into the background.
Then while the user enters a commit message the tests run and the commit-msg hook checks
for the outcome of the tests.
CONFIGURATION
cehgit tries to load the following configuration files in this order:
"~/config/cehgit.conf" "~/.cehgit.conf" ".git/cehgit.conf" ".cehgit.conf"
They are all optional, the defaults should be sufficient for most use cases. When not, then
one can create the one config file and customize it. Only '.cehgit.conf' are meant to be
versioned and distriubuted. The others adds local configuration that is not versioned.
Following Variables can be configured [=default]:
$(sed 's/ *declare -gx \([^ ]*\)=\([^ ]*\) *#G *\(.*\)/ \1 [\2]\n \3 \n/p;d' < "$0")
WRITING MODULES
Modules extend the cehgit API. They are loaded with the 'require' function.
All modules must be prefixed with 'mod-' and be in ACTIONS_DIR. The first word after 'mod-'
should be a descriptive name of what the module does.
Modules should define function to be used by actions. They should not run any code on their
own except for initialization tasks. Ideally they use the 'memo'/'memofn' function to cache state.
Functions they define should contain with the module name and a descriptive name of what the
function does. The usual form is 'mod_*' where '*' is a descriptive name of what the
function does. For primary predicates we allow 'if_mod_*' forms too.
WRITING ACTIONS
cehgit runs all actions in order and aborts execution on the first failure.
Actions must be prefixed with a double digit number to ensure proper ordering.
It is recommended to follow following guides for the naming:
- 10 configuration and prepopulation
When some adjustments are to be done to make the test dir compileable this is the place.
- 20 validate test dir, toolchain versions
Check for presence and validty of files in the test dir.
Check for toolchains/tools, required versions
- 30 linters/formatting check
Testing begins here with resonable cheap checks, running linters and similar things.
- 40 building
This is where compilation happens, we are building the project, tests here. But do
not run them yet.
- 50 normal testing
Runs the standard testsuite. The shipped actions will background these tests.
- 60 extensive testing/mutants/fuzzing
When there are any expensive tests like running mutant checks, fuzzing, benchmarks this
is done here. The shipped actions will background these tests.
- 70 doc
Generate documentation, possibly test it.
- 80 staging work, release chores, changelogs
When some automatic workflow should be implemented like promoting/merging branches,
generating changelogs etc, this is done here.
- 90 packaging/deploy/postprocessing
Final step for automatic workflows which may build packages and deploy it.
Also if any final processing/cleanup has to be done.
Actions should have comments starting with '##' which will be extracted on 'cehgit list-actions'
and 'cehgit available-actions' giving some info about what an action does.
Actions can do different things:
- Calling functions that check whenever the action should be in effect or not. cehgit calls
all actions in order. Some actions should only run under certain conditions. Each action
may return early when it should not run. The shopped modules provides functions to check
the current git branch or the hook that is running and to schedule actions to run in the
background and retrieve its results. These functions check for some conditions and return
1 when the condition is not met. This must be handled by the caller otherwise
cehgit will exit with a failure:
git_branch_matches master || return 0
git_hook_matches pre-commit || return 0
background_schedule && return 0
background_wait && return \$(background_result)
...
- Call an actual test. This is usually done by the run_test function that is part of cehgit
API. It takes the command to run as parameters and runs this in a subshell with some
(configurable) resource limits applied. On a Makefile based project this may be something
like:
run_test make check
Actions are run within the TEST_DIR being the current directory.
AVAILABLE ACTIONS
Some actions is shipped with cehgit. More will be added in future. These
implement and eventually evolve into an automated workflow.
$($0 available-actions | awk '{print " " $0}')
BUILTIN ACTION FUNCTIONS
cehgit provides a minimal set of built-in functions to be used in actions. Most functionality
should be implemented in modules.
These functions return 0 on success and 1 on failure. A 'return 1' must be handled otherwise
cehgit would exit. Ususally this is done by something like 'some_action_function || return
0'. Using a 'if' or other operators is possible as well.
$(sed 's/^\(function \([^ ]*\) *\)\?#afunc \(\([^-]*\) *-\)\? *\(.*\)/ \2 \4\n \5\n/p;d' < "$0")
API FUNCTIONS
We define a few functions for diagnostics, locking, caching and to record states:
$(sed 's/^function \([^ ]*\) *#api \([^-]*\) *- *\(.*\)/ \1 \2\n \3\n/p;d' < "$0")
For further documentation look into the source.
INITIAL INSTALLATION
cehgit can be invoked in 3 ways:
1. Installed in \$PATH:
This is used to initialize cehgit in git repositories and install actions and hooks.
'cehgit init' copies itself to './.cehgit' and creates a './.cehgit.d/' directory
when called in a git repository.
The recommended way to install cehgit in \$PATH
2. The local './.cehgit' initialized from above:
This should be versioned, so anyone who clones a git repository where cehgit is
initialized can use this local version.
3. githooks symlinked from './.git/hooks/*' -> '../../.cehgit'
When called as githook, then it calls actions in './.cehgit.d/' in order.
To make 1. work it is best to clone cehgit locally and symlink the checked out files
to your '.local/' tree. This allows easy upgrades via git:
# clone the repository and change into it
git clone https://git.pipapo.org/cehgit
cd cehgit
# symlink the script itself
ln -s $PWD/cehgit $HOME/.local/bin/
# symlink the actions directory
mkdir -p $HOME/.local/share/cehgit
ln -s $PWD/actions $HOME/.local/share/cehgit/
You can manually copy or symlink either from above to '/usr/bin' and '/usr/share' as well.
SECURITY
cehgit is completely inert in a initialized or freshly checked out repository. One always
has to './.cehgit install-hook' to enable it. Then as any other build script cehgit actions
run in the context of the calling user. Unlike in a CI there is no isolation. Thus before
hooks are enabled the user is responsible to check or trust the shipped actions.
LICENSE
cehgit -- cehtehs personal git assistant
Copyright (C) 2024 Christian Thäter <ct.cehgit@pipapo.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
EOF
exit 0
}
function require () {
declare -gA MODULE_LOADED
declare -ga MODULE_PATH=(".cehgit.d")
declare -g MODULE_PREFIX="mod-"
for mod in "$@"; do
if [[ ${MODULE_LOADED["$mod"]:-false} = false ]]; then
if [[ "$mod" = */* && -f "$mod" ]]; then
debug "$mod"
source "$mod"
MODULE_LOADED["$mod"]=true
else
for path in "${MODULE_PATH[@]}"; do
if [[ -f "$path/$MODULE_PREFIX$mod" ]]; then
debug "$path/$MODULE_PREFIX$mod"
source "$path/$MODULE_PREFIX$mod"
MODULE_LOADED["$mod"]=true
break
elif [[ -f "$path/$mod" ]]; then
debug "$path/$mod"
source "$path/$mod"
MODULE_LOADED["$mod"]=true
break
fi
done
fi
if [[ ${MODULE_LOADED["$mod"]:-false} = false ]]; then
die "failed to load $mod"
fi
else
trace "already loaded: $mod"
fi
done
}
function memo {
if [[ $1 == "-c" ]]; then
unset MEMO_RC MEMO_STDOUT MEMO_STDERR
shift
fi
local delete=false
if [[ $1 == "-d" ]]; then
delete=true
shift
fi
declare -gAi MEMO_RC
declare -gA MEMO_STDOUT MEMO_STDERR
local key="$(sha256sum <<<"$$""$*")"
key="${key:0:64}"
if [[ $delete = true && -v MEMO_RC["$key"] ]]; then
unset "MEMO_RC[$key]" "MEMO_STDOUT[$key]" "MEMO_STDERR[$key]"
fi
if [[ -v MEMO_RC["$key"] ]]; then
echo -n "${MEMO_STDOUT[$key]}"
echo -n "${MEMO_STDERR[$key]}" 1>&2
return ${MEMO_RC["$key"]}
elif [[ $# -ge 1 ]]; then
local errexit=false
[[ $- == *e* ]] && errexit=true
set +e
touch "/tmp/$key.stdout" "/tmp/$key.stderr"
eval "$*" > >(tee "/tmp/$key.stdout") 2> >(tee "/tmp/$key.stderr" 1>&2)
local rc=$?
[[ $errexit = true ]] && set -e
MEMO_RC["$key"]=$rc
IFS= read -r -d '' MEMO_STDOUT["$key"] < "/tmp/$key.stdout"
IFS= read -r -d '' MEMO_STDERR["$key"] < "/tmp/$key.stderr"
rm "/tmp/$key.stdout" "/tmp/$key.stderr"
return "$rc"
fi
}
function memofn {
for fn in "$@"; do
eval "nomemo_$(declare -f "$fn")"
eval "function $fn () { memo nomemo_$fn \"\$@\" ; }"
done
}
function memo_ok {
memo "$@" || die "'$*' failed with $?"
}
function source_info {
echo "${BASH_SOURCE[$((${1:-0}+1))]}:${BASH_LINENO[$((${1:-0}))]}:${FUNCNAME[$((${1:-0}+1))]:+${FUNCNAME[$((${1:-0}+1))]}:}"
}
function die {
if [[ $VERBOSITY_LEVEL -gt 0 ]]; then
echo -e "\033[1;91mPANIC:\033[0m $(source_info 1) $*" >&2
fi
exit 1
}
function error {
if [[ $VERBOSITY_LEVEL -gt 0 ]]; then
echo -e "\033[1;31mERROR:\033[0m $(source_info 1) $*" >&2
fi
}
function note {
if [[ $VERBOSITY_LEVEL -gt 1 ]]; then
echo -e "\033[1;35m NOTE:\033[0m $*" >&2
fi
}
function info {
if [[ $VERBOSITY_LEVEL -gt 2 ]]; then
echo -e "\033[1;34m INFO:\033[0m $*" >&2
fi
}
function debug {
if [[ $VERBOSITY_LEVEL -gt 3 ]]; then
echo -e "\033[1;36mDEBUG:\033[0m $(source_info 1) $*" >&2
fi
}
function trace {
if [[ $VERBOSITY_LEVEL -gt 4 ]]; then
echo -e "\033[1;96mTRACE:\033[0m $(source_info 1) $*" >&2
fi
}
if [[ ${SHTEST_TESTSUITE:-false} = false ]]; then
cehgit "$@"
fi