#!/bin/bash
shopt -s nullglob
IFS=$'\n\b'

# die hard, see https://github.com/capr/die
say()   { echo "$@" >&2; }
die()   { echo -n "ABORT: " >&2; echo "$@" >&2; exit 1; }
debug() { [ "$DEBUG" ] && echo "$@" >&2; }
run()   { debug -n "EXEC: $@ "; "$@"; local ret=$?; debug "[$ret]"; return $ret; }
must()  { debug -n "MUST: $@ "; "$@"; local ret=$?; debug "[$ret]"; [ $ret = 0 ] || die "$@ [$ret]"; }
dry()   { if [ "$DRY" ]; then [ "$VERBOSE" ] && say "$@"; else "$@"; fi; }

usage() {
	say
	say " multigit 5.0 - git wrapper for working with overlaid repos."
	say " Cosmin Apreutesei | public domain | https://github.com/capr/multigit"
	say
	say " USAGE: mgit [OPTIONS...] COMMAND ..."
	say
	say "   ls                           list cloned repos"
	say "   ls-all                       list all known repos"
	say "   ls-uncloned                  list all known but not cloned repos"
	say
	say "   ls-modified|st[atus]         list modified files across all repos"
	say "   ls-unpushed                  list repos that are ahead of origin"
	say "   ls-untracked                 list files untracked by any repo"
	say "   ls-double-tracked            list files tracked by multiple repos"
	say "   ls-tracked                   list files and which repos are tracking them"
	say "   which FILENAME               list which repo(s) are tracking a file"
	say "   diff FILENAME                call git diff FILENAME on FILENAME's repo"
	say
	say "   init REPO                              create a local repo"
	say "   clone [REMOTE/]REPO|URL[=VERSION] ...  clone one ore more repos"
	say "   clone-all                              clone all known uncloned repos"
	say "   clone-release REL|RELFILE              clone/checkout all repos from a release (file)"
	say "   remove REPO ...                        remove repos from disk (!)"
	say "   convert [NAME]                         convert current git repo to mgit"
	say
	say "   baseurl [REMOTE [URL|-]]     get/set/delete the baseurl of a remote"
	say "   origin [REPO [REMOTE|URL|-]] get/set/delete the known origin of a repo"
	say
	say "   [-] REPO1,...                       start a shell for using git on a repo"
	say "   [-] REPO1,...|--all COMMAND ...     execute any git command on a repo"
	say "   [-] REPO1,...|--all exec ...        execute a shell command in a repo context"
	say "   [-] REPO1,...|--all ver[sion] [tag] show repo version or tag (as enum or list)"
	say "   [-] REPO1,...|--all make-symlinks   make symbolic links in .mgit/REPO"
	say "   [-] REPO1,...|--all make-hardlinks  make hard links in .mgit/REPO"
	say
	say "   release [REL]                show a release or list releases"
	say "   release REL update [tag]     create/update a release based on HEADs"
	say "   release REL clone            clone/checkout all repos from a release"
	say "   release REL remove           remove a release file"
	say
	say "   bash                         drop to bash (even on Windows)"
	say "   [help|--help]                show this screen"
	say
	say " OPTIONS:"
	say
	say "   -v                           verbose"
	say "   --debug                      print commands"
	say "   --dry                        don't actually remove stuff"
	say "   --yes                        choose yes when asked to remove stuff"
	say "   -SS                          reuse SSH connections (Linux only)"
	say "   -P N                         number of parallel processes for clone (1)"
	say
	# append any plugin help files
	for f in .mgit/*.help; do
		cat "$f"
	done
	exit
}

check_root() { [ -d .mgit ] || die "'.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
	"$0" --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
}

which_on() { # repo file
	GIT_DIR=".mgit/$1/.git" git ls-files --error-unmatch "$2" >/dev/null 2>/dev/null || return 1
	echo "$1"
}

which_tracks() {
	[ "$1" ] || die "Filename expected."
	local s="${1##*/}"  # strip path
	local s="${s##*\\}" # strip path
	local guess1="${s%.*}" # strip .suffix
	local guess2="${s%_*}" # strip _suffix
	[ "$guess1" != "$1" -a -d ".mgit/$guess1/.git" ] && which_on "$guess1" "$1" && return 0
	[ "$guess2" != "$1" -a -d ".mgit/$guess2/.git" ] && which_on "$guess2" "$1" && return 0
	list_cloned | while IFS=" " read repo; do
		which_on "$repo" "$1" && return 0
	done
	return 1
}

diff_file() {
	local file="${@: -1}"
	local repo="$(which_tracks "$file")" || return 1
	"$0" "$repo" diff "$@"
}

existing_files() {
	find * -type f | grep -v "^$1$" | sort
}

list_tracked() {
	check_root
	local MGIT_DIR="$PWD"
	for repo in `list_cloned`; do
		(cd "$PWD0"; GIT_DIR="$MGIT_DIR/.mgit/$repo/.git" git ls-files) | while read f; do
			printf "%-16s %s\n" "$repo" "$f"
		done
	done
}

list_sub() { # list_sub A B removes from list A the lines found in list B.
	# NOTE: using a temp file because bash on Windows can't do <(tracked_files)
	"$2" | sort > $$.tmp
	if [ "$OSTYPE" != "msys" ]; then
		# NOTE: not using grep because it's buggy on OSX (doesn't match all).
		"$1" $$.tmp | comm -23 - $$.tmp
	else
		# NOTE: using grep because Git for Windows doesn't have comm,
		# and because if MSYS is in PATH, comm from MSYS can't be used with
		# Git's bash (it crashes).
		"$1" $$.tmp | grep -v -F -x -f $$.tmp
	fi
	rm $$.tmp
}

list_untracked() {
	check_root
	list_sub existing_files tracked_files
}

list_double_tracked() {
	check_root
	# NOTE: using a temp file because bash on Windows can't do <(tracked_files -d)
	tracked_files -d > $$.tmp
	[ "$(cat $$.tmp)" ] && \
		PWD0="$PWD" list_tracked | grep -F -w -f $$.tmp
	rm $$.tmp
}

clone_all() {
	for repo in `list_uncloned`; do
		"$0" clone "$repo"
	done
}

check_repo_name() {
	[ "$1" ] || die "Repo name expected."
	local name="$1"
	name="${name//[^\.\-_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]/}"
	[ "$name" = "$1" ] || die "Invalid name: '$1'. Only letters, numbers, '.', '-' and '_' allowed."
	name="${name//^[\.\-]/}"
	[ "$name" = "$1" ] || die "Invalid name: '$1'. Must not start with '.' or '-'."
}

init() {
	check_repo_name "$1"
	local name="$1"

	[ -d ".mgit/$name" ] && die "Already cloned: '$name'."

	must mkdir -p ".mgit/$name"
	export GIT_DIR=".mgit/$name/.git"
	must git init $GITOPT $MULTIGIT_INIT_OPTS >&2
	must git config core.worktree ../../..

	must git config core.excludesfile ".mgit/$name.exclude"
	must make_exclude_file "$name"
}

make_exclude_file() {
	[ "$no_exclude_file" ] && return
	[ -f ".mgit/$1.exclude" ] && return
	# make a default "exclude-all" exclude file
	echo '*' > ".mgit/$1.exclude"
}

clone_one() {
	local arg="$1"
	local name
	local origin
	local rorigin
	local url
	local ver

	arg="${arg//[[:blank:]]/}" # spaces not allowed
	[ "$arg" = "$1" ] || die "$name is an invalid 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" ] || die "$name is an invalid name"

	export GIT_DIR=".mgit/$name/.git"

	# if if repo is already cloned do a checkout instead.
	[ -d ".mgit/$name" ] && {
		local ver0="$(git_ver_for "$name")"
		[ "$ver" -a "$ver" = "$ver0" ] && {
			[ "$VERBOSE" ] && printf "SKIP: %-16s already at %s\n" "$name" "$ver"
			exit 0
		}
		printf "PULL:  %-16s %-7s %s\n" "$name" "$ver" "(was: $ver0)" >&2
		must git fetch $GITOPT $MULTIGIT_FETCH_OPTS >&2
		if [ "$ver" ]; then
			must git -c advice.detachedHead=false checkout $GITOPT "$ver" >&2
		else
			must git checkout $GITOPT -B master origin/master >&2
		fi
		exit 0
	}

	# check for a registered origin
	[ -f ".mgit/$name.origin" ] && \
		rorigin=$(cat .mgit/$name.origin)

	# decide the origin
	if [ "$origin" ]; then
		[ "$rorigin" -a "$origin" != "$rorigin" ] && \
			say "NOTE:  $name using different origin: '$origin' (was '$rorigin')."
	else
		origin="$rorigin"
		[ "$origin" ] || die "$name has no origin file"
	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
				say "ABORT: $name has unknown origin: '$origin'."
				say "HINT:  To register '$origin' to be used as an origin, type, eg.:"
				say "HINT:    "$(basename "$0")" baseurl $origin https://github.com/$origin/"
				exit 1
			fi
		fi
	fi

	# finally, clone the repo
	(
		printf "CLONE: %-16s %s\n" "$name" "$ver" >&2
		no_exclude_file=1 must init "$name"
		must git remote add origin "$url"
		must git fetch $GITOPT $MULTIGIT_FETCH_OPTS >&2
	) || {
		# cleanup on failed fetch because git doesn't.
		rm -rf ".mgit/$name/"
		rm -f ".mgit/$name.exclude"
		die "$name clone failed"
	}
	if run git rev-parse $GITOPT --verify origin/master >/dev/null; then
		run git branch $GITOPT --track master origin/master >&2
		must git -c advice.detachedHead=false checkout $GITOPT $ver >&2
	else # new repo
		must git checkout $GITOPT -b master
		git config branch.master.remote origin
		git config branch.master.merge refs/heads/master
	fi
	# make an exclude file if one wasn't checked out already.
	must make_exclude_file "$name"

	# (re)register the repo's origin.
	if [ "$origin" != "$rorigin" ]; then
		if [ "$rorigin" ]; then
			say "NOTE:  $name has new origin: '$origin' (was: '$rorigin')"
		else
			[ "$VERBOSE" ] && {
				say "NOTE:  $name has origin: '$origin'"
				say "       (url: $url)"
			}
		fi
		must mkdir -p .mgit
		echo "$origin" > ".mgit/$name.origin" \
			|| die "Could not create origin file .mgit/$name.origin".
	fi
}

clone_from_stdin() {
	xargs -i -P "$MAXPROC" -- "$0" clone "{}"
}

clone() {
	[ "$1" ] || die "Repo name expected."
	if [ "$1" = "-" ]; then
		clone_from_stdin
	elif [ $# = 1 ]; then
		clone_one "$1"
	else
		while [ $# != 0 ]; do
			printf "%s\n" "$1"
			shift
		done | clone_from_stdin
	fi
}

remove_one() {

	[ -d ".mgit/$1/" ] || die "$1 is not a cloned repo"

	# don't remove from a subshell
	[ "$MULTIGIT_REPO" = "$1" ] && die "Refusing to remove '$1' from a subshell."

	# get tracked files for this repo
	files="$(GIT_DIR=".mgit/$1/.git" git ls-files)" || {
		say "ABORT: Could not get the list of files for '$1'."
		say "HINT:  If you know that there are no checked out files,"
		say "HINT:  feel free to \`rm -rf .mgit/$1/ .mgit/$1.exclude\`."
		exit 1
	}

	# ask for confirmation if there are files to delete
	[ "$files" -a -z "$YES" ] && {
		local n=$(echo "$files" | wc -l)
		say "Remove ALL $((n)) files of '$1'? You can't undo this [yes/N]"
		read yes
		[ "$yes" = "yes" ] || { say "Canceled."; exit 1; }
	}

	# remove files
	for file in $files; do
		dry rm "$file"
	done

	# remove empty directories
	for file in $files; do
		echo "$(dirname "$file")"
	done | uniq | while read dir; do
		[ "$dir" != "." ] && dry /bin/rmdir -p "$dir" 2>/dev/null
	done

	# remove the git dir
	dry rm -rf ".mgit/$1/"
	dry rm  -f ".mgit/$1.exclude"

	say "REMOVED: $1"
}

remove() {
	[ "$1" ] || die "Repo name expected."
	if [ $# = 1 ]; then
		remove_one "$1"
	else
		while true; do
			case "$1" in
				--dry)    export DRY=1; shift ;;
				--yes)    export YES=1; shift ;;
				*)        break ;;
			esac
		done
		while [ $# != 0 ]; do
			"$0" remove "$1"
			shift
		done
	fi
}

convert() {
	local name="$1"
	[ "$name" ] || name="$(basename "$PWD")"
	check_repo_name "$name"
	[ -d ".mgit/$name" ] && {
		[ -d "$PWD0/.git" ] && die "$name already taken"
		[ "$VERBOSE" ] && say "SKIP:  $name already in multigit"
		exit 0
	}
	[ -d "$PWD0/.git" ] || die "No .git dir in current dir"
	must mkdir -p ".mgit/$name"
	must mv "$PWD0/.git" ".mgit/$name/.git"
	export GIT_DIR=".mgit/$name/.git"
	must git config core.worktree ../../..
	must git config core.excludesfile ".mgit/$name.exclude"
	must make_exclude_file "$name"
	say "CONVERTED: $name"
}

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" ] && die "Invalid origin name '$1'."
	[ "$2" -a "$2" != "$url" ] && die "Invalid baseurl '$2'."

	if [ "X$url" = "X-" ]; then
		rm ".mgit/$origin.baseurl"
	elif [ "$url" ]; then
		[ "${url##*/}" != "" ] && die "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" ] && die "Invalid repo name '$1'."
	[ "$2" -a "$2" != "$origin" ] && die "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
}

release_file() {
	local rel="$1"
	[ "$rel" ] || die "Release name or file expected."
	[ "${rel##*.}" = "release" -a -f "$rel" ] || rel=".mgit/$rel.release"
	[ "$2" = "nocheck" -o -f "$rel" ] || die "Release not found: '$1'."
	echo "$rel"
}

show_release() {
	local f; f="$(release_file "$1")" || exit; cat "$f"
}

update_release() {
	local rel; rel="$(release_file "$1" nocheck)" || exit
	if [ -f "$rel" ]; then
		local s="$(cat "$rel")"
		echo "$s" | (IFS=" "; while read repo ver0; do
			local name="${repo##*/}"
			local ver="$(git_ver_for "$name" $2)"
			[ "$ver0" = "*" ] && ver="*" # not touching those
			printf "%-20s %s\n" "$repo" "$ver"
			[ "$ver" != "$ver0" ] && printf "%-20s %s -> %s\n" "$repo" "$ver0" "$ver" >&2
		done) > "$rel"
	else
		"$0" --all version $2 > "$rel"
	fi
}

remove_release() {
	local f; f="$(release_file "$1")" || exit; rm "$f"
}

list_release_repos() {
	printf "%s\n" "$RELEASE" | (IFS=" "; while read repo ver; do
		local name="${repo##*/}"
		printf "%s\n" "$name"
	done)
}

clone_release() {
	local rel; rel="$(release_file "$1")" || exit
	local s="$(cat "$rel")" # load it in memory to make sure we have it till the end.

	# step 1: remove any repos that are not part of the release.
	RELEASE="$s" list_sub list_cloned list_release_repos | while read repo; do
		"$0" --yes remove "$repo"
	done

	# step 2: clone/checkout repos present in the release.
	printf "%s\n" "$s" | (IFS=" "; while read repo ver; do
			# Version "*" in a release file specifies that the repo is already cloned
			# and checked out: this is the repo that contains the release file itself.
			# Even if cloned manually beforehand, the repo that contains the release file
			# must still be listed in the release file to avoid removing it in step 1.
			if [ "$ver" != "*" ]; then
				printf "%s\n" "$repo=$ver"
			fi
		done
	) | "$0" clone -
	[ $? = 0 ] || die "Failed."
}

release() {
	local rel="$1"
	local cmd="$2"
	rel="${rel//[[:blank:]]/}" # spaces not allowed
	[ "$rel" -a "$1" != "$rel" ] && die "Invalid release name '$1'."
	[ "$rel" ] || { list_releases; return; }
	shift 2
	case "$cmd" in
		"")       show_release   "$rel" "$@" ;;
		update)   update_release "$rel" "$@" ;;
		clone)    clone_release  "$rel" "$@" ;;
		remove)   remove_release "$rel" "$@" ;;
		*)        die "Invalid release command '$cmd'."
	esac
}

git_shell() {
	cd "$PWD0" || return
	say "Entering subshell: git commands will affect the repo '$MULTIGIT_REPO'."
	say "Type \`exit' to exit subshell."
	git status -s
	say
	if [ "$OSTYPE" = "msys" -a "$MULTIGIT_USE_CMD" ]; then
		export PROMPT="[$MULTIGIT_REPO] \$P\$G"
		"$COMSPEC" /k
	else
		export PS1="[$MULTIGIT_REPO] \u@\h:\w\$ "
		"$SHELL" --norc -i
	fi
}

git_ver_for() {
	if [ "$2" = "tag" ]; then
		GIT_DIR=".mgit/$1/.git" git describe --tags --abbrev=0 2>/dev/null $3
	else
		GIT_DIR=".mgit/$1/.git" git describe --tags --long --always $3
	fi
}

git_ver() {
	printf "$MULTIGIT_REPO=$(git_ver_for "$MULTIGIT_REPO" $1) "
}

git_version() {
	printf "%-20s %s" "$MULTIGIT_REPO"
	git_ver_for "$MULTIGIT_REPO" $1
}

git_remove_links() {
	[ "$OSTYPE" = "msys" ] && die "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_one() {
	local repo="$1"
	local cmd="$2"
	shift 2
	[ "$VERBOSE" ] && say "@$repo:"
	export GIT_DIR="$PWD/.mgit/$repo/.git"
	export MULTIGIT_REPO="$repo"
	[ -d "$GIT_DIR" ] || die "Unknown repo: '$repo'."
	case "$cmd" in
		exec)           cd "$PWD0" && "$@" ;;
		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
				PWD0="$PWD0" ".mgit/git-$cmd.sh" "$@"
			else
				(cd "$PWD0" && git "$cmd" "$@")
			fi
		;;
	esac
}

git_cmd() {
	local repos="$1"; shift
	if [ "$repos" = "--all" ]; then
		[ "$1" ] || die "Refusing to start a subshell for each repo."
		for repo in `list_cloned`; do
			git_cmd_one "$repo" "$@"
		done
	else
		(IFS=","; for repo in $repos; do
			git_cmd_one "$repo" "$@"
		done)
	fi
	[ "$1" = "ver" ] && say
}

cd_root() {
	local pwd1
	while [ "$PWD" != "$pwd1" ]; do
		[ -d .mgit ] && return
		pwd1="$PWD"
		cd .. || die "Could not cd to '$PWD/..'."
	done
	cd "$PWD0" # root dir not found, go back to initial dir
}

PWD0="$PWD"
cd_root

while true; do
	case "$1" in
		-v)       shift; export VERBOSE=1;;
		--debug)  shift; export DEBUG=1;;
		--dry)    shift; export DRY=1;;
		--yes)    shift; export YES=1;;
		-SS)      shift; [ "$OSTYPE" != "msys" ] && export GIT_SSH_COMMAND="ssh -o ControlMaster=auto -o ControlPersist=10 -o ControlPath=~/.ssh/sock-mgit";;
		-P)       shift; export MAXPROC="$1"; shift;;
		*)        break;;
	esac
done
[ "$DEBUG" ] || export GITOPT=-q
[ "$MAXPROC" ] || export MAXPROC=1
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 "$@" ;;
	st)           list_modified "$@" ;;
	status)       list_modified "$@" ;;
	which)        which_tracks "$@" ;;
	diff)         diff_file "$@" ;;
	ls-unpushed)  list_unpushed ;;
	ls-untracked) list_untracked ;;
	ls-double-tracked) list_double_tracked ;;
	ls-tracked)   list_tracked ;;
	init)         init "$@" ;;
	clone)        clone "$@" ;;
	clone-all)    clone_all "$@" ;;
	clone-release) clone_release "$@" ;;
	remove)       remove "$@" ;;
	convert)      convert "$@" ;;
	baseurl)      baseurl "$@" ;;
	origin)       origin "$@" ;;
	release)      release "$@" ;;
	bash)         cd "$PWD0" && exec bash --norc "$@" ;;
	-)            git_cmd "$@" ;;
	*)
		# look for and execute a plugin command
		if [ -f ".mgit/$cmd.sh" ]; then
			".mgit/$cmd.sh" "$@"
		else
			git_cmd "$cmd" "$@"
		fi
		;;
esac
