#!/usr/bin/env bash inverse=$(tput rev) reset=$(tput sgr0) txtbld=$(tput bold) bldred=${txtbld}$(tput setaf 1) # default option settings guardedmode=false singlemode=false allwsmode=false quiet=false no_follow_symlinks=false no_follow_hidden=false # # print usage message # usage() { echo 1>&2 "usage: git bulk [--no-follow-symlinks] [--no-follow-hidden] [-q|--quiet] [-g] ([-a]|[-w ]) " echo 1>&2 " git bulk --addworkspace (--from )" echo 1>&2 " git bulk --removeworkspace " echo 1>&2 " git bulk --addcurrent " echo 1>&2 " git bulk --purge" echo 1>&2 " git bulk --listall" } cdfail() { echo 1>&2 "failed to change directory: $1" exit 1 } # add another workspace to global git config addworkspace() { git config --global bulkworkspaces."$wsname" "$wsdir"; if [ -n "$source" ]; then if [ ! -d "$wsdir" ]; then echo 1>&2 "Path of workspace doesn't exist, make it first."; exit 1; fi regex='http(s)?://|ssh://|(git@)?.*:.*/.*' if [[ "$source" =~ $regex ]]; then pushd "$wsdir" > /dev/null || cdfail "$wsdir" git clone "$source" popd > /dev/null || cdfail "$OLDPWD" else source=$(realpath "$source" 2>/dev/null) if [ -f "$source" ]; then pushd "$wsdir" > /dev/null || cdfail "$wsdir" while IFS= read -r line; do if [ -n "$line" ]; then # the git clone command to take the complete line in the repository.txt as separate argument. This facilitated the cloning of the repository with a custom folder name. # shellcheck disable=SC2086 git clone $line; fi done < "$source" popd > /dev/null || cdfail "$OLDPWD" else echo 1>&2 "format of URL or file unknown" fi fi fi } # add current directory addcurrent() { git config --global bulkworkspaces."$wsname" "$PWD"; } # remove workspace from global git config removeworkspace() { checkWSName && git config --global --unset bulkworkspaces."$wsname"; } # remove workspace from global git config purge() { git config --global --remove-section bulkworkspaces; } # list all current workspace locations defined listall() { git config --global --get-regexp bulkworkspaces; } # guarded execution of a git command in one specific repository guardedExecution () { if [ "${quiet?}" != "true" ] || $guardedmode; then echo 1>&2 "${bldred}->${reset} executing ${inverse}git $gitcommand${reset} in repository ${leadingpath%/*}/${bldred}${curdir##*/}${reset}" fi if $guardedmode; then echo 1>&2 -n " Execute command here (y/n)? " read -n 1 -r &2 if [[ $REPLY =~ ^[Yy]$ ]]; then git "$@" fi else git "$@" fi } # check if the passed command is known as a core git command checkGitCommand () { if git help -a | grep -o -q "\b${corecommand}\b"; then echo 1>&2 "Core command \"$corecommand\" accepted." else if git config --get-regexp alias | grep -o -q "\.${corecommand} "; then echo 1>&2 "Alias ${corecommand} accepted." else usage && echo 1>&2 "error: unknown GIT command: $corecommand" && exit 1 fi fi } # check if workspace name is registered checkWSName () { while read -r workspace; do parseWsName "$workspace" if [[ $rwsname == "$wsname" ]]; then return; fi done <<< "$(listall)" # when here the ws name was not found usage && echo 1>&2 "error: unknown workspace name: $wsname" && exit 1 } # parse out wsname from workspacespec parseWsName () { local wsspec="$1" # Get the workspace value from its specification in the `.gitconfig`. # May be an absolute path or a variable name of the form: `$VARNAME` rwsdir=${wsspec#* } if [[ ${rwsdir:0:1} == '$' ]]; then # Dereference the `rwsdir` value which is a variable name. rwsdir_varname=${rwsdir:1} rwsdir=${!rwsdir_varname} if [[ -z "${rwsdir}" ]]; then echo 1>&2 "error: bad environment variable: $rwsdir_varname" && exit 1 fi fi rwsname=${wsspec#*.} && rwsname=${rwsname%% *} } # detects the wsname of the current directory wsnameToCurrent () { while read -r workspace; do if [ -z "$workspace" ]; then continue; fi parseWsName "$workspace" if echo "$PWD" | grep -o -q "$rwsdir"; then wsname="$rwsname" && return; fi done <<< "$(listall)" # when here then not in workspace dir echo 1>&2 "error: you are not in a workspace directory. your registered workspaces are:" && \ wslist="$(listall)" && echo 1>&2 "${wslist:-''}" && exit 1 } # helper to check number of arguments. allowedargcount () { if [ "$paramcount" -ne "${1:-0}" ] && [ "$paramcount" -ne "${2:-0}" ]; then echo 1>&2 "error: wrong number of arguments" && usage; exit 1; fi } # execute the bulk operation executBulkOp () { checkGitCommand if ! $allwsmode && ! $singlemode; then wsnameToCurrent; fi # by default git bulk works within the 'current' workspace listall | while read -r workspacespec; do parseWsName "$workspacespec" if [[ -n $wsname ]] && [[ $rwsname != "$wsname" ]]; then continue; fi cd "$rwsdir" || exit 1 local actual=$PWD [ "${quiet?}" != "true" ] && echo 1>&2 "Executing bulk operation in workspace ${inverse}$actual${reset}" # build `find` flags depending on command-line options local find_flags=() if [[ "$no_follow_symlinks" == true ]]; then find_flags+=(-P) else find_flags+=(-L) fi # find all git repositories under the workspace on which we want to operate readarray allGitFolders < <(find "${find_flags[@]}" . -name ".git" 2>/dev/null) for line in "${allGitFolders[@]}"; do local gitrepodir=${line::${#line}-5} # cut the .git part of find results to have the root git directory of that repository cd "$gitrepodir" || exit 1 # into git repo location local curdir=$PWD local leadingpath=${curdir#"${actual}"} # do not execute if we do not want to consider a ".git" directory under a hidden directory if [ $no_follow_hidden = false ] || ! [[ "$leadingpath" =~ "/." ]]; then guardedExecution "$@" fi cd "$rwsdir" || exit 1 # back to origin location of last find command done done } paramcount="${#}" # if no arguments show usage if [[ $paramcount -le 0 ]]; then usage; fi # parse command parameters while [ "${#}" -ge 1 ] ; do case "$1" in --quiet|-q) quiet='true' ;; --listall|--purge) butilcommand="${1:2}" && break ;; --removeworkspace|--addcurrent|--addworkspace) butilcommand="${1:2}" && wsname="$2" && wsdir="$3" && if [ "$4" = "--from" ]; then source="$5"; fi && break ;; --no-follow-symlinks) no_follow_symlinks=true ;; --no-follow-hidden) no_follow_hidden=true ;; -a) allwsmode=true ;; -g) guardedmode=true ;; -w) singlemode=true && shift && wsname="$1" && checkWSName ;; --*) usage && echo 1>&2 "error: unknown argument $1" && exit 1 ;; -*) usage && echo 1>&2 "error: unknown argument $1" && exit 1 ;; *) # git core commands butilcommand="executBulkOp" && corecommand="$1" && gitcommand="$*" && break ;; esac && shift done # check option compatibility if $allwsmode && $singlemode; then echo 1>&2 "error: options -w and -a are incompatible" && exit 1; fi # if single mode check the supplied workspace name if $singlemode; then echo 1>&2 "Selected single workspace mode in workspace: $wsname" && checkWSName; fi # check right number of arguments case $butilcommand in listall|purge) allowedargcount 1;; addcurrent|removeworkspace) allowedargcount 2;; addworkspace) allowedargcount 3 5;; esac # pass the origin arguments to the 'executBulkOp' $butilcommand "$@" # run user command