mirror of
https://github.com/tj/git-extras.git
synced 2026-09-10 07:26:17 -04:00
* fix(git-bulk): fix workspace selection when cd fails
`cd` may fails for multiple reasons:
- mistake when editing `.gitconfig` manually
- previously existing workspace that have been removed
- ...
Currently, if `cd` fails, the `BulkOp` continue its execution ... in the
workspace defined in a higher directory that where the user, despite the
user specified a specific workspace (`-w`).
The user should be noticed of a failed `cd` (this is really not expected
for a valid configuration) and the operations should stop to not execute
something unexpected.
* fix(git-bulk): replace weak eval for better variable substitution
Get rid of poor `eval` syntax because they are vulnerable to command
injection, which may have unexpected side effects.
However, they enabled a useful feature: using environment variable
(*e.g.*, defined in a `.bashrc`) inside the `.gitconfig` to use dynamic
paths as `bulk` workspaces.
As such, I keep this feature possible by using the Bash's ${!VAR}
syntax, which allows to get the value of one variable using the name of
a another variable. However, arbitrary command injection is not possible
anymore.
* fix(git-bulk): missing check about empty environnement variable
* style(git-bulk): typo
* docs(Commands.md): git-bulk env var feature
* docs(man/git-bulk): git-bulk env var feature
* docs(man/git-bulk): mention .gitconfig for config storage
* style(man/git-bulk): typo
* docs(man/git-bulk): run make/ronn for .1 and .html
215 lines
7.2 KiB
Bash
Executable file
215 lines
7.2 KiB
Bash
Executable file
#!/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
|
|
|
|
#
|
|
# print usage message
|
|
#
|
|
usage() {
|
|
echo 1>&2 "usage: git bulk [-q|--quiet] [-g] ([-a]|[-w <ws-name>]) <git command>"
|
|
echo 1>&2 " git bulk --addworkspace <ws-name> <ws-root-directory> (--from <URL or file>)"
|
|
echo 1>&2 " git bulk --removeworkspace <ws-name>"
|
|
echo 1>&2 " git bulk --addcurrent <ws-name>"
|
|
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
|
|
function 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
|
|
function addcurrent { git config --global bulkworkspaces."$wsname" "$PWD"; }
|
|
|
|
# remove workspace from global git config
|
|
function removeworkspace { checkWSName && git config --global --unset bulkworkspaces."$wsname"; }
|
|
|
|
# remove workspace from global git config
|
|
function purge { git config --global --remove-section bulkworkspaces; }
|
|
|
|
# list all current workspace locations defined
|
|
function listall { git config --global --get-regexp bulkworkspaces; }
|
|
|
|
# guarded execution of a git command in one specific repository
|
|
function 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 </dev/tty; echo 1>&2
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
git "$@"
|
|
fi
|
|
else
|
|
git "$@"
|
|
fi
|
|
}
|
|
|
|
# check if the passed command is known as a core git command
|
|
function 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
|
|
function 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
|
|
function 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
|
|
function 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:-'<no workspaces defined yet>'}" && exit 1
|
|
}
|
|
|
|
# helper to check number of arguments.
|
|
function 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
|
|
function 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}"
|
|
|
|
allGitFolders=( $(eval find -L . -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}"}
|
|
guardedExecution "$@"
|
|
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 ;;
|
|
-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
|