Files
multigit/mgit
T
Cosmin Apreutesei ef84772940 unimportant
2015-04-09 09:45:35 +03:00

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