mirror of
https://github.com/vxcontrol/multigit.git
synced 2026-07-19 18:23:34 -04:00
529 lines
12 KiB
Bash
Executable File
529 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
shopt -s nullglob
|
|
IFS=$'\n\b'
|
|
|
|
usage() {
|
|
[ "$1" ] && {
|
|
echo
|
|
echo "ERROR: $1"
|
|
echo
|
|
exit
|
|
}
|
|
echo
|
|
echo " multigit 1.0 - git wrapper for working with overlaid repos."
|
|
echo " Cosmin Apreutesei | public domain | https://github.com/capr/multigit"
|
|
echo
|
|
echo " USAGE: $0 ..."
|
|
echo
|
|
echo " ls list cloned repos"
|
|
echo " ls-all list all known repos"
|
|
echo " ls-uncloned list all known but not cloned repos"
|
|
echo
|
|
echo " ls-modified list files that were modified locally"
|
|
echo " ls-unpushed list repos that are ahead of origin"
|
|
echo " ls-untracked list files untracked by any repo"
|
|
echo " ls-double-tracked list files tracked by multiple repos"
|
|
echo
|
|
echo " init REPO [git-init-options...] create a local repo"
|
|
echo " clone [REMOTE/]REPO|URL[=version] ... clone one ore more repos"
|
|
echo " clone-all [git-fetch-options] clone all known uncloned repos"
|
|
echo " remove [--dry] REPO ... remove cloned repos from disk (!)"
|
|
echo
|
|
echo " baseurl [REMOTE [URL|-]] get/set/delete the baseurl of a remote"
|
|
echo " origin [REPO [REMOTE|URL|-]] get/set/delete the known origin of a repo"
|
|
echo
|
|
echo " REPO start a shell for using git on a repo"
|
|
echo " REPO|--all command ... execute any git command on a repo"
|
|
echo " REPO|--all exec ... execute a shell command in a repo context"
|
|
echo " REPO|--all ver[sion] show repo version (enum or list)"
|
|
echo " REPO|--all make-symlinks make symbolic links in .mgit/REPO"
|
|
echo " REPO|--all make-hardlinks make hard links in .mgit/REPO"
|
|
echo
|
|
echo " release [REL] show a release or list releases"
|
|
echo " release REL update create/update a release based on HEADs"
|
|
echo " release REL checkout clone/checkout repos from a release"
|
|
echo
|
|
echo " [help|--help] show this screen"
|
|
echo
|
|
# append any plugin help files
|
|
for f in .mgit/*.help; do
|
|
cat "$f"
|
|
done
|
|
echo
|
|
exit
|
|
}
|
|
|
|
check_root() { [ -d .mgit ] || usage "'.mgit' dir not found."; }
|
|
|
|
list_known() {
|
|
check_root
|
|
(cd .mgit && \
|
|
for f in *.origin; do
|
|
echo "${f%.origin}"
|
|
done)
|
|
}
|
|
|
|
list_cloned() {
|
|
check_root
|
|
(cd .mgit && \
|
|
for f in *; do
|
|
[ -d "$f/.git" ] && echo "$f"
|
|
done)
|
|
}
|
|
|
|
list_uncloned() {
|
|
check_root
|
|
(cd .mgit && \
|
|
for f in *.origin; do
|
|
f="${f%.origin}"
|
|
[ ! -d "$f/.git" ] && echo $f
|
|
done)
|
|
}
|
|
|
|
list_modified() {
|
|
check_root
|
|
git_cmd_all status -s
|
|
}
|
|
|
|
list_unpushed() {
|
|
check_root
|
|
for repo in `list_cloned`; do
|
|
[ "$(GIT_DIR=".mgit/$repo/.git" \
|
|
git rev-list HEAD...origin/master --count 2>/dev/null)" \
|
|
!= "0" ] && echo "$repo"
|
|
done
|
|
}
|
|
|
|
tracked_files() {
|
|
check_root
|
|
([ -d .git ] && git ls-files
|
|
for repo in `list_cloned`; do
|
|
GIT_DIR=".mgit/$repo/.git" git ls-files
|
|
done) | sort | uniq $1
|
|
}
|
|
|
|
existing_files() {
|
|
find * -type f | sort | uniq
|
|
}
|
|
|
|
list_untracked() {
|
|
check_root
|
|
comm -23 <(existing_files) <(tracked_files)
|
|
}
|
|
|
|
list_double_tracked() {
|
|
check_root
|
|
for repo in `list_cloned`; do
|
|
GIT_DIR=".mgit/$repo/.git" git ls-files | while read f; do
|
|
printf "%-16s %s\n" "$repo" "$f"
|
|
done
|
|
done | grep -F -w -f <(tracked_files -d)
|
|
}
|
|
|
|
clone_all() {
|
|
export MULTIGIT_FETCH_OPTS="$@"
|
|
for repo in `list_uncloned`; do
|
|
"$0" clone "$repo"
|
|
done
|
|
}
|
|
|
|
init() {
|
|
local name="$1"
|
|
name="${name//[^-_0123456789abcdefghijklmnopqrstuvwxyz]/}"
|
|
[ "$name" = "$1" ] || \
|
|
usage "Invalid name: '$1'. Only letters, numbers, '-' and '_' allowed."
|
|
shift
|
|
|
|
# check that the repo is not already cloned
|
|
[ -d ".mgit/$name" ] && {
|
|
echo "SKIPPING: Already exists: '$name'."
|
|
return 1
|
|
}
|
|
|
|
mkdir -p ".mgit/$name"
|
|
export GIT_DIR=".mgit/$name/.git"
|
|
git init "$@"
|
|
git config --local core.worktree ../../..
|
|
|
|
# make a default "exclude-all" exclude file
|
|
git config --local core.excludesfile ".mgit/$name.exclude"
|
|
[ -f ".mgit/$name.exclude" ] || echo '*' > ".mgit/$name.exclude"
|
|
|
|
return 0
|
|
}
|
|
|
|
clone_one() {
|
|
local arg="$1"
|
|
local name
|
|
local origin
|
|
local rorigin
|
|
local url
|
|
local ver
|
|
|
|
arg="${arg//[[:blank:]]/}" # spaces not allowed
|
|
[ "$arg" = "$1" ] || usage "Invalid name '$name'."
|
|
|
|
# extract `=version` if any
|
|
ver="${arg##*=}"
|
|
[ "$ver" == "$arg" ] && ver=""
|
|
arg="${arg%=*}"
|
|
|
|
# check if the arg is a full url or just `[origin/]name`
|
|
if [ "${arg#*:}" != "$arg" ]; then
|
|
url="$arg"
|
|
origin="$arg"
|
|
name="${url##*/}"
|
|
name="${name%.git}"
|
|
else
|
|
name="${arg##*/}"
|
|
origin="${arg%/*}"
|
|
[ "$origin" = "$arg" ] && origin=""
|
|
fi
|
|
|
|
# check that arg is not `/` or `origin/`
|
|
[ "$name" ] || usage "Invalid repo name '$1'."
|
|
|
|
# check for a registered origin
|
|
[ -f ".mgit/$name.origin" ] && \
|
|
rorigin=$(cat .mgit/$name.origin)
|
|
|
|
# decide the origin
|
|
if [ "$origin" ]; then
|
|
[ "$rorigin" -a "$origin" != "$rorigin" ] && \
|
|
echo "NOTE: Using different origin for '$name': '$origin' (was '$rorigin')."
|
|
else
|
|
origin="$rorigin"
|
|
[ "$origin" ] || {
|
|
echo "SKIPPING: Unknown repo '$name'."
|
|
return
|
|
}
|
|
fi
|
|
|
|
# find the origin url
|
|
if [ ! "$url" ]; then
|
|
if [ -f ".mgit/$origin.baseurl" ]; then
|
|
local baseurl=$(cat .mgit/$origin.baseurl)
|
|
url="$baseurl$name"
|
|
else
|
|
# assume the origin on file is a full url: check if it is
|
|
if [ "${origin#*:}" != "$origin" ]; then
|
|
url="$origin"
|
|
else
|
|
echo "ERROR: Unknown origin: '$origin' for '$name'."
|
|
echo "HINT: To register '$origin' to be used as an origin, type, eg.:"
|
|
echo "HINT: "$(basename "$0")" baseurl $origin https://github.com/$origin/"
|
|
return
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# finally, clone the repo
|
|
init "$name" || return
|
|
git remote add origin "$url"
|
|
git fetch $MULTIGIT_FETCH_OPTS || {
|
|
rm -rf ".mgit/$name/"
|
|
rm ".mgit/$name.exclude"
|
|
echo "ERROR: Fetch failed. Repo removed."
|
|
return
|
|
}
|
|
git branch --track master origin/master
|
|
local branch="$ver"; [ "$branch" ] || branch=master
|
|
git checkout -B $branch $ver
|
|
|
|
# if fetch was successful, (re)register the repo's origin
|
|
if [ "$origin" != "$rorigin" ]; then
|
|
if [ "$rorigin" ]; then
|
|
echo "NOTE: Updating origin for '$name': '$origin' (was '$rorigin')"
|
|
else
|
|
echo "NOTE: Adding origin for '$name': '$origin' (url: '$url')"
|
|
fi
|
|
mkdir -p .mgit
|
|
echo "$origin" > ".mgit/$name.origin"
|
|
fi
|
|
}
|
|
|
|
clone() {
|
|
[ "$1" ] || usage "Repo name expected."
|
|
if [ $# = 1 ]; then
|
|
clone_one "$@"
|
|
else
|
|
while [ $# != 0 ]; do
|
|
clone_one "$1"
|
|
shift
|
|
done
|
|
fi
|
|
}
|
|
|
|
run() { if [ "$DRY_RUN" ]; then echo "$@"; else "$@"; fi; }
|
|
|
|
remove_one() {
|
|
[ "$1" ] || usage "Invalid name '$1'."
|
|
|
|
[ -d ".mgit/$1/" ] || {
|
|
echo "ERROR: Repo not found '$1'."
|
|
return
|
|
}
|
|
|
|
# don't remove from a subshell
|
|
[ "$MULTIGIT_REPO" == "$1" ] && \
|
|
usage "Refusing to remove '$1' from a subshell."
|
|
|
|
# get tracked files for this repo
|
|
files="$(GIT_DIR=".mgit/$1/.git" git ls-files)" || {
|
|
echo "ERROR: Could not get the list of files for '$1'."
|
|
echo "HINT: If you know that there are no checked out files,"
|
|
echo "HINT: feel free to \`rm -rf .mgit/$1/ .mgit/$1.exclude\`."
|
|
return
|
|
}
|
|
|
|
# remove files
|
|
for file in $files; do
|
|
run rm "$file"
|
|
done
|
|
|
|
# remove empty directories
|
|
for file in $files; do
|
|
echo "$(dirname "$file")"
|
|
done | uniq | while read dir; do
|
|
[ "$dir" != "." ] && run /bin/rmdir -p "$dir" 2>/dev/null
|
|
done
|
|
|
|
# remove the git dir
|
|
run rm -rf ".mgit/$1/"
|
|
run rm -f ".mgit/$1.exclude"
|
|
|
|
echo "Removed: '$1'."
|
|
}
|
|
|
|
remove() {
|
|
[ "$1" = "--dry" ] && { DRY_RUN=1; shift; }
|
|
[ "$1" ] || usage "Repo name expected."
|
|
if [ $# = 1 ]; then
|
|
remove_one "$@"
|
|
else
|
|
while [ $# != 0 ]; do
|
|
remove_one "$1"
|
|
shift
|
|
done
|
|
fi
|
|
}
|
|
|
|
baseurl() {
|
|
[ "$1" ] || {
|
|
for f in .mgit/*.baseurl; do
|
|
f="${f#.mgit/}"
|
|
f="${f%.baseurl}"
|
|
printf "%-20s %s\n" "$f" "$(baseurl "$f")"
|
|
done
|
|
return
|
|
}
|
|
local origin="$1"
|
|
local url="$2"
|
|
# spaces not allowed
|
|
origin="${origin//[[:blank:]]/}"
|
|
url="${url//[[:blank:]]/}"
|
|
[ -z "$1" -o "$1" != "$origin" ] && usage "Invalid origin name '$1'."
|
|
[ "$2" -a "$2" != "$url" ] && usage "Invalid baseurl '$2'."
|
|
|
|
if [ "X$url" = "X-" ]; then
|
|
rm ".mgit/$origin.baseurl"
|
|
elif [ "$url" ]; then
|
|
[ "${url##*/}" != "" ] && usage "A base URL must end with a '/'."
|
|
mkdir -p .mgit
|
|
echo "$url" > ".mgit/$origin.baseurl"
|
|
else
|
|
cat ".mgit/$origin.baseurl"
|
|
fi
|
|
}
|
|
|
|
origin() {
|
|
[ "$1" ] || {
|
|
for f in .mgit/*.origin; do
|
|
f="${f#.mgit/}"
|
|
f="${f%.origin}"
|
|
printf "%-20s %s\n" "$f" "$(origin "$f")"
|
|
done
|
|
return
|
|
}
|
|
local repo="$1"
|
|
local origin="$2"
|
|
|
|
# spaces not allowed
|
|
repo="${repo//[[:blank:]]/}"
|
|
origin="${origin//[[:blank:]]/}"
|
|
[ -z "$1" -o "$1" != "$repo" ] && usage "Invalid repo name '$1'."
|
|
[ "$2" -a "$2" != "$origin" ] && usage "Invalid origin '$2'."
|
|
|
|
if [ "X$origin" = "X-" ]; then
|
|
rm ".mgit/$repo.origin"
|
|
elif [ "$origin" ]; then
|
|
mkdir -p .mgit
|
|
echo "$origin" > ".mgit/$repo.origin"
|
|
else
|
|
cat ".mgit/$repo.origin"
|
|
fi
|
|
}
|
|
|
|
list_releases() {
|
|
for f in .mgit/*.release; do
|
|
f="${f#.mgit/}"
|
|
f="${f%.release}"
|
|
echo "$f"
|
|
done
|
|
}
|
|
|
|
show_release() {
|
|
[ -f ".mgit/$1.release" ] || usage "Unknown release '$1'."
|
|
cat ".mgit/$1.release"
|
|
}
|
|
|
|
update_release() {
|
|
"$0" --all version > ".mgit/$1.release"
|
|
}
|
|
|
|
checkout_release() {
|
|
local repo="$1"
|
|
[ -f ".mgit/$repo.release" ] || usage "Unknown release '$repo'."
|
|
cat ".mgit/$repo.release" | (IFS=" "; while read repo ver; do
|
|
[ -d ".mgit/$repo/.git" ] || "$0" clone "$repo"
|
|
local branch="$ver"; [ "$branch" ] || branch=master
|
|
"$0" "$repo" checkout -B "$branch" "$ver"
|
|
done)
|
|
}
|
|
|
|
release() {
|
|
local rel="$1"
|
|
rel="${rel//[[:blank:]]/}" # spaces not allowed
|
|
[ "$rel" -a "$1" != "$rel" ] && usage "Invalid release name '$1'."
|
|
[ "$rel" ] || { list_releases; return; }
|
|
case "$2" in
|
|
"") show_release "$rel" ;;
|
|
update) update_release "$rel" ;;
|
|
checkout) checkout_release "$rel" ;;
|
|
*) usage "Invalid release commnad '$2'."
|
|
esac
|
|
}
|
|
|
|
git_shell() {
|
|
echo "Entering subshell: git commands will affect the repo '$MULTIGIT_REPO'."
|
|
echo "Type \`exit' to exit subshell."
|
|
git status -s
|
|
echo
|
|
if [ "$OSTYPE" = "msys" ]; then
|
|
export PROMPT="[$MULTIGIT_REPO] \$P\$G"
|
|
exec "$COMSPEC" /k
|
|
else
|
|
export PS1="[$MULTIGIT_REPO] \u@\h:\w\$ "
|
|
exec "$SHELL" -i
|
|
fi
|
|
}
|
|
|
|
git_ver() {
|
|
printf "$MULTIGIT_REPO=$(git describe --tags --long --always) "
|
|
[ "$ALL" ] || echo
|
|
}
|
|
|
|
git_version() {
|
|
printf "%-20s" "$MULTIGIT_REPO"
|
|
git describe --tags --long --always
|
|
}
|
|
|
|
git_remove_links() {
|
|
[ "$OSTYPE" = "msys" ] && usage "Not for Windows."
|
|
([ "$MULTIGIT_REPO" ] && cd ".mgit/$MULTIGIT_REPO" || exit 1
|
|
find . ! -path './.git/*' ! -path './.git' ! -path '.' -exec rm -rf {} \; 2>/dev/null)
|
|
}
|
|
git_make_hardlinks() {
|
|
git_remove_links
|
|
git ls-files | while read f; do
|
|
mkdir -p "$(dirname ".mgit/$MULTIGIT_REPO/$f")"
|
|
ln -f "$f" ".mgit/$MULTIGIT_REPO/$f"
|
|
done
|
|
}
|
|
git_make_symlinks() {
|
|
git_remove_links
|
|
git ls-files | while read f; do
|
|
mkdir -p "$(dirname ".mgit/$MULTIGIT_REPO/$f")"
|
|
ln -sf "$PWD/$f" ".mgit/$MULTIGIT_REPO/$f"
|
|
done
|
|
}
|
|
|
|
git_cmd() {
|
|
local repo="$1"
|
|
local cmd="$2"
|
|
shift 2
|
|
export GIT_DIR=".mgit/$repo/.git"
|
|
export MULTIGIT_REPO="$repo"
|
|
[ -d "$GIT_DIR" ] || usage "Unknown repo: '$repo'."
|
|
case "$cmd" in
|
|
exec) "$@" ;;
|
|
ver) git_ver ;;
|
|
version) git_version ;;
|
|
make-symlinks) git_make_symlinks ;;
|
|
make-hardlinks) git_make_hardlinks ;;
|
|
"") git_shell ;;
|
|
*)
|
|
# look for and execute a git plugin command
|
|
if [ -f ".mgit/git-$cmd.sh" ]; then
|
|
".mgit/git-$cmd.sh" "$@"
|
|
else
|
|
git "$cmd" "$@"
|
|
fi
|
|
;;
|
|
esac
|
|
export GIT_DIR=
|
|
export MULTIGIT_REPO=
|
|
}
|
|
|
|
git_cmd_all() {
|
|
for repo in `list_cloned`; do
|
|
ALL=1 git_cmd "$repo" "$@"
|
|
done
|
|
[ "$1" == "ver" ] && echo
|
|
}
|
|
|
|
cd_root() {
|
|
local pwd1
|
|
while [ "$PWD" != "$pwd1" ]; do
|
|
[ -d .mgit ] && return
|
|
pwd1="$PWD"
|
|
cd .. || usage "Could not cd to '$PWD/..'."
|
|
done
|
|
cd "$PWD0" # root dir not found, go back to initial dir
|
|
}
|
|
|
|
PWD0="$PWD"
|
|
cd_root
|
|
|
|
cmd="$1"; shift
|
|
case "$cmd" in
|
|
"") usage ;;
|
|
help) usage ;;
|
|
--help) usage ;;
|
|
ls) list_cloned ;;
|
|
ls-all) list_known ;;
|
|
ls-uncloned) list_uncloned ;;
|
|
ls-modified) list_modified ;;
|
|
ls-unpushed) list_unpushed ;;
|
|
ls-untracked) list_untracked ;;
|
|
ls-double-tracked) list_double_tracked ;;
|
|
init) init "$@" ;;
|
|
clone) clone "$@" ;;
|
|
remove) remove "$@" ;;
|
|
baseurl) baseurl "$@" ;;
|
|
origin) origin "$@" ;;
|
|
release) release "$@" ;;
|
|
--all)
|
|
[ "$1" ] || usage "Refusing to start a subshell for each repo."
|
|
git_cmd_all "$@"
|
|
;;
|
|
*)
|
|
# look for and execute a plugin command
|
|
if [ -f ".mgit/$cmd.sh" ]; then
|
|
".mgit/$cmd.sh" "$@"
|
|
else
|
|
git_cmd "$cmd" "$@"
|
|
fi
|
|
;;
|
|
esac
|