From: alex Date: Sat, 14 Dec 2024 17:29:12 +0000 (-0800) Subject: initial project completion X-Git-Tag: v1.0.0^0 X-Git-Url: http://git.infiniteadaptability.org/?a=commitdiff_plain;h=1eac03b07eec444a02ad19099b60a01ca8566b8a;p=mirror-all initial project completion --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07d19e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +test/source.sh +test/.testdir diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dbe3391 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin + +all: + @echo "mirror-all is a shell script; nothing to make. Try 'make install' instead." + +check: + @head -n `grep -n "main()" mirror-all | cut -f1 -d:` mirror-all | head -n -1 > test/source.sh + @cd test && run-parts --exit-on-error --regex '.*\.tests\.sh' . + +clean: + @rm -vrf test/.testdir + +install: + @install -v -d "$(BINDIR)/" && install -m 0755 -v mirror-all "$(BINDIR)/mirror-all" + +uninstall: + @rm -vrf \ + $(BINDIR)/mirror-all + +.PHONY: install uninstall check clean diff --git a/manifest.scm b/manifest.scm index 6859940..2505962 100644 --- a/manifest.scm +++ b/manifest.scm @@ -1,7 +1,8 @@ -(specifications->manifest - (list "bash" - "coreutils" - "git" - "make" - "openssh" - "shellcheck")) +(specifications->manifest (list "bash" + "coreutils" + "debianutils" + "git" + "grep" + "make" + "openssh" + "shellcheck")) diff --git a/mirror-all b/mirror-all new file mode 100755 index 0000000..2cda380 --- /dev/null +++ b/mirror-all @@ -0,0 +1,205 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# option variables +LOG_LEVEL=default + +# binaries +GIT="git" +SSH="ssh" +LS="ls" + +# helper variables +TARGET= +SERVER= +TO_CLONE=() +TO_FETCH=() + +detect_git() { + log_info "detecting git" + if [[ -z "$("$GIT" --version)" ]]; then + log_err "git not found in path" + exit 1 + fi +} + +detect_ls() { + log_info "detecting ls" + if [[ -z "$("$LS" --version)" ]]; then + log_err "ls not found in path" + exit 1 + fi +} + +detect_ssh() { + log_info "detecting ssh" + if [[ -z "$("$SSH" -V 2>&1)" ]]; then + log_err "ssh not found in path" + exit 1 + fi +} + +fetch_repos() { + local name + for repo in "${TO_FETCH[@]}"; do + name="$(basename "$repo")" + (cd "$name" && "$GIT" fetch --prune --prune-tags && git update-server-info) > /dev/null 2>&1 + done + + log_info "fetched ${#TO_FETCH[@]} repos" +} + +find_local_directories() { + local repos + + log_info "finding all git repos in $TARGET" + + readarray -d '' repos < <("$LS" --zero "$TARGET") + + for i in "${repos[@]}"; do + set +e + local target="$TARGET/${i}" + if [[ "true" == "$("$GIT" -C "$target" rev-parse --is-bare-repository 2>&1)" ]]; then + if [[ -d "$i" ]]; then + TO_FETCH+=("$target") + else + TO_CLONE+=("$target") + fi + else + log_info "$target is not a bare git repository, skipping" + unset + fi + set -e + done +} + +find_remote_directories() { + local repos + + log_info "finding all git repos in $SERVER:$TARGET" + + readarray -d '' repos < <("$SSH" "$SERVER" -- "$LS" "$TARGET" --zero) + + for i in "${repos[@]}"; do + set +e + local target="$TARGET/${i}" + if [[ "true" == "$("$SSH" "$SERVER" -- "$GIT" -C "$target" rev-parse --is-bare-repository 2>&1)" ]]; then + if [[ -d "$i" ]]; then + TO_FETCH+=("$SERVER:$target") + else + TO_CLONE+=("$SERVER:$target") + fi + else + log_info "$target is not a git repo, skipping" + fi + set -e + done +} + +clone_repos() { + local name + for repo in "${TO_CLONE[@]}"; do + name="$(basename "$repo")" + "$GIT" clone --mirror "$repo" "$name" > /dev/null 2>&1 + echo "$name" > "$name/description" + (cd "$name" && "$GIT" update-server-info) + done + + log_info "cloned ${#TO_CLONE[@]} repos" +} + +log() { + if [[ "$LOG_LEVEL" != "error" ]]; then + echo -e "$@" + fi +} + +log_err() { + echo -e "$@" +} + +log_info() { + if [[ "$LOG_LEVEL" == "info" ]]; then + log "$@" + fi +} + +parse_arguments() { + # transform long options into short opts + for arg in "$@"; do + shift + case "$arg" in + "--quiet") set -- "$@" "-q" ;; + "--verbose") set -- "$@" "-v" ;; + *) set -- "$@" "$arg" ;; + esac + done + + # parse short options + OPTIND=1 + while getopts "qv" opt + do + case "$opt" in + "q") set_loglevel "error" ;; + "v") set_loglevel "info" ;; + "?") usage ;; + esac + done + + shift $(( OPTIND - 1 )) # remove options + if [[ "$#" -eq 1 ]]; then + # remove trailing slash (if it exists) + TARGET="${1%/}" + elif [[ "$#" -eq 2 ]]; then + SERVER="$1" + TARGET="${2%/}" + else + log_err "Requires at least one argument" + exit 1 + fi +} + +set_loglevel() { + log_info "setting loglevel to '$1'" + case "$1" in + "default") LOG_LEVEL="default" ;; + "info") LOG_LEVEL="info" ;; + "error") LOG_LEVEL="error" ;; + *) log_err "unknown log level '$1'" ;; + esac +} + +usage() { + log_err "Usage:" + log_err "" + log_err "\tmirror-all [options] [SERVER] [TARGET]" + log_err "" + log_err "Options:" + log_err "\t--quiet, -q" + log_err "\t--verbose, -v" + exit 1 +} + +main() { + parse_arguments "$@" + + detect_git + + if [[ -n "$SERVER" ]]; then + detect_ssh + find_remote_directories + else + detect_ls + find_local_directories + fi + + clone_repos + fetch_repos + + log "Successfully updated repos" + + exit +} + +main "$@" diff --git a/test/clone_repos.tests.sh b/test/clone_repos.tests.sh new file mode 100755 index 0000000..248b92f --- /dev/null +++ b/test/clone_repos.tests.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# this command will fail if not executed in test/ directory +source setup.sh + +setup_env + +TO_CLONE+=("../repos/repo3") +LOG_LEVEL=info + +cd mirrors + +clone_repos > result 2>&1 +EXPECTED="cloned 1 repos" +RESULT=$( /dev/null 2>&1 +PREV="$(cd repo3 && git rev-parse HEAD)" + +cd ../repos/repo1 +echo "alskdjflasidf" > test4.txt +git add test4.txt > /dev/null 2>&1 +git commit -m ... > /dev/null 2>&1 +NEXT="$(git rev-parse HEAD)" +cd ../../mirrors + +refute "$PREV" "$NEXT" + +fetch_repos > result 2>&1 +EXPECTED="fetched 1 repos" +RESULT=$( result 2>&1 +EXPECTED=$'finding all git repos in ../repos\n' +EXPECTED+=$'../repos/notarepo is not a bare git repository, skipping\n' +EXPECTED+="../repos/repo1 is not a bare git repository, skipping" +RESULT=$( temp) +USAGE=$( "$TEST_DIR/repos/notarepo/test1" + mkdir -p "$TEST_DIR/mirrors" + cd "$TEST_DIR/repos" + git init --bare repo2 + git init repo1 + cd repo1 + echo "hello" > test.txt + echo "laskdjfalskdfj" > test2.txt + mkdir -p "hi/test/nope/what" + echo "alskdjflaksdjflkasjdflkasjdklasjdlkfjd" > "hi/test/nope/what/test3.txt" + git config user.email "mirror-all-test@infiniteadaptability.org" + git config user.name "mirror-all-test" + git add test.txt + git add test2.txt + git add hi/test/nope/what/test3.txt + git commit -m "initial commit" + cd .. + git clone --mirror repo1 repo3 + cd .. +} > /dev/null 2>&1 + +reset_env() { + clean_env + setup_env +} + +assert() { + if [[ "$1" != "$2" ]]; then + echo -e "Expected:\n$1\nResult:\n$2" + echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} failed" + exit 1 + fi +} + +refute() { + if [[ "$1" == "$2" ]]; then + echo "refute failed!" + echo -e "Expected:\n$1\nResult:\n$2" + echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} failed" + exit 1 + fi +} + +test_failed() { + echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} failed" + exit 1 +} + +test_succeeded() { + echo "${BASH_SOURCE[1]} succeeded" + clean_env +}