--- /dev/null
+#!/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 "$@"
--- /dev/null
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# this command will fail if not executed in test/ directory
+source setup.sh
+
+setup_env
+
+TO_FETCH+=("../repos/repo3")
+LOG_LEVEL=info
+
+cd mirrors
+
+git clone --mirror ../repos/repo1 repo3 > /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)
+assert "$EXPECTED" "$RESULT"
+
+assert "$NEXT" "$(cd repo3 && git rev-parse HEAD)"
+
+cd ..
+
+test_succeeded
--- /dev/null
+#!/usr/bin/env bash
+
+TEST_DIR=".testdir"
+
+source source.sh
+
+set +e
+(usage > temp)
+USAGE=$(<temp)
+rm -f temp
+set -e
+
+clean_env() {
+ cd ..
+ rm -rf "$TEST_DIR"
+}
+
+setup_env() {
+ mkdir -p "$TEST_DIR/repos/notarepo"
+ echo "thisisatest" > "$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.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
+}