#! /bin/sh set -efu main() {( self=$(readlink -f "$0") basename=${0##*/} debug=false force=false origin_host=${HOSTNAME-cat /proc/sys/kernel/hostname} origin_user=$LOGNAME target_spec= fail=true error() { echo "$basename: error: $1" >&2 fail=false } for arg; do case $arg in --force) force=true ;; -*) error "bad argument: $arg" ;; *) if test -n "$target_spec"; then error "bad argument: $arg" else target_spec=$arg fi ;; esac done if test -z "$target_spec"; then error 'no target specified' fi if test "$fail" != true; then exit 11 fi target=$( echo "$target_spec" | jq -R ' def default(value; f): if . == null then value else f end; def default(value): default(value; .); match("^(?:([^@]+)@)?([^:/]+)(?::([^/]*))?(/.*)?") | { user: .captures[0].string | default("root"), host: .captures[1].string, port: .captures[2].string | default(22; if test("^[0-9]+$") then fromjson else error(@json "bad target port: \(.)") end), path: .captures[3].string | default("/var/src"), } ' ) target_host=$(echo $target | jq -r .host) target_path=$(echo $target | jq -r .path) target_port=$(echo $target | jq -r .port) target_user=$(echo $target | jq -r .user) if test "$force" = true; then force_target else check_target fi jq -c 'to_entries | group_by(.value.type) | flatten[]' | while read -r source; do key=$(echo "$source" | jq -r .key) type=$(echo "$source" | jq -r .value.type) case $type in file) file_path=$(echo "$source" | jq -r .value.file.path) populate_file "$key" "$file_path" ;; git) git_url=$(echo "$source" | jq -r .value.git.url) git_ref=$(echo "$source" | jq -r .value.git.ref) populate_git "$key" "$git_url" "$git_ref" ;; symlink) symlink_target=$(echo "$source" | jq -r .value.symlink.target) populate_symlink "$key" "$symlink_target" ;; *) echo "Warning: ignoring $source" >&2 ;; esac done )} # Safeguard to prevent clobbering of misspelled targets. # This function has to be called first. check_target() { { echo target_host=$(quote "$target_host") echo target_path=$(quote "$target_path") echo 'sentinel_file=$target_path/.populate' echo 'if ! test -f "$sentinel_file"; then' echo ' echo "error: missing sentinel file: $target_host:$sentinel_file" >&2' echo ' exit 1' echo 'fi' } \ | target_shell } force_target() { { echo target_path=$(quote "$target_path") echo 'sentinel_file=$target_path/.populate' echo 'mkdir -vp "$target_path"' echo 'touch "$sentinel_file"' } \ | target_shell } is_local_target() { test "$target_host" = "$origin_host" && test "$target_user" = "$origin_user" } populate_file() {( print_info populate_file "$@" file_name=$1 file_path=$2 if is_local_target; then file_target=$target_path/$file_name else file_target=$target_user@$target_host:$target_path/$file_name fi rsync \ -vFrlptD \ --delete-excluded \ "$file_path"/ \ -e "ssh -o ControlPersist=no -p $target_port" \ "$file_target" )} populate_git() {( print_info populate_git "$@" git_name=$1 git_url=$2 git_ref=$3 git_work_tree=$target_path/$git_name { echo set -efu echo git_name=$(quote "$git_name") echo git_url=$(quote "$git_url") echo git_ref=$(quote "$git_ref") echo git_work_tree=$(quote "$git_work_tree") echo 'if ! test -e "$git_work_tree"; then' echo ' git clone "$git_url" "$git_work_tree"' echo 'fi' echo 'cd $git_work_tree' echo 'if ! url=$(git config remote.origin.url); then' echo ' git remote add origin "$git_url"' echo 'elif test "$url" != "$git_url"; then' echo ' git remote set-url origin "$git_url"' echo 'fi' # TODO resolve git_ref to commit hash echo 'hash=$git_ref' echo 'if ! test "$(git log --format=%H -1)" = "$hash"; then' echo ' if ! git log -1 "$hash" >/dev/null 2>&1; then' echo ' git fetch origin' echo ' fi' echo ' git checkout "$hash" -- "$git_work_tree"' echo ' git -c advice.detachedHead=false checkout -f "$hash"' echo 'fi' echo 'git clean -dfx' } \ | target_shell )} populate_symlink() {( print_info populate_symlink "$@" symlink_name=$1 symlink_source=$2 symlink_target=$target_path/$symlink_name { # TODO rm -fR instead of ln -f? echo ln -fns $(quote "$symlink_source" "$symlink_target") } \ | target_shell )} print_info() { printf '\e[1;33m* %s\e[m\n' "$*" >&2 } quote() { printf %s "$1" | sed 's/./\\&/g' while test $# -gt 1; do printf ' ' shift printf %s "$1" | sed 's/./\\&/g' done echo } target_shell() { if is_local_target; then /bin/sh else ssh "$target_host" \ -l "$target_user" \ -o ControlPersist=no \ -p "$target_port" \ -T \ /bin/sh fi } main "$@"