unimportant

This commit is contained in:
Cosmin Apreutesei
2021-11-07 21:53:06 +02:00
parent b3895bbd28
commit 5cf27e4a01
2 changed files with 178 additions and 174 deletions
+6 -6
View File
@@ -2,17 +2,17 @@
## Layered Git Repositories | **Linux**, **Windows**, **Mac**
Multigit allows checking out multiple git repositories overlaid
Multigit allows checking out multiple git repositories overlaid
onto the same directory.
It is useful for projects which are made of different components
that are developed separately, but which need to track files
that are developed separately, but which need to track files
from different parts of the directory structure of the project.
This cannot be done using git submodules or git subtrees, which
only allow subprojects to be checked out into their own private
subdirectories. Multigit allows repositories to track any file
from any directory of the project structure, similar to a union
only allow subprojects to be checked out into their own private
subdirectories. Multigit allows repositories to track any file
from any directory of the project structure, similar to a union
filesystem, where each repository is a layer.
Some examples where this combination of change management
@@ -220,7 +220,7 @@ This is such basic and useful functionality that it should
be built into `git clone` and `git init` really. As dead simple
as multigit is, it's still yet another script that you have to deploy.
## Simple? But it's a 600 lines script!
## Simple? But it's a 700 lines script!
Don't worry about it, it's mostly fluff. The gist of it is only 6 lines:
+172 -168
View File
@@ -2,68 +2,73 @@
shopt -s nullglob
IFS=$'\n\b'
# die hard, see https://github.com/capr/die
say() { echo "$@" >&2; }
die() { echo -n "EXIT: " >&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 say "$@"; else "$@"; fi; }
usage() {
[ "$1" ] && {
echo
echo "ERROR: $1"
echo
exit
}
echo
echo " multigit 3.9.0 - git wrapper for working with overlaid repos."
echo " Cosmin Apreutesei | public domain | https://github.com/capr/multigit"
echo
echo " USAGE: mgit [OPTIONS...] COMMAND..."
echo
echo " -v verbose"
echo " -SS reuse SSH connections (Linux only)"
echo " --dry don't actually remove stuff"
echo " --yes choose yes when asked to remove stuff"
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|st[atus] list modified files across all repos"
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 " ls-tracked list files and which repos are tracking them"
echo " which FILENAME list which repo(s) are tracking a file"
echo
echo " init REPO create a local repo"
echo " clone [REMOTE/]REPO|URL[=VERSION] ... clone one ore more repos"
echo " clone-all clone all known uncloned repos"
echo " clone-release REL|RELFILE clone/checkout all repos from a release (file)"
echo " remove REPO ... remove repos from disk (!)"
echo " convert [NAME] convert current git repo to mgit"
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 " [-] REPO1,... start a shell for using git on a repo"
echo " [-] REPO1,...|--all command ... execute any git command on a repo"
echo " [-] REPO1,...|--all exec ... execute a shell command in a repo context"
echo " [-] REPO1,...|--all ver[sion] [tag] show repo version or tag (as enum or list)"
echo " [-] REPO1,...|--all make-symlinks make symbolic links in .mgit/REPO"
echo " [-] REPO1,...|--all make-hardlinks make hard links in .mgit/REPO"
echo
echo " release [REL] show a release or list releases"
echo " release REL update [tag] create/update a release based on HEADs"
echo " release REL clone clone/checkout all repos from a release"
echo " release REL remove remove a release file"
echo
echo " [help|--help] show this screen"
echo
say
say " multigit 4.0b - 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
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
# 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."; }
check_root() { [ -d .mgit ] || die "'.mgit' dir not found."; }
list_known() {
check_root
@@ -113,7 +118,7 @@ tracked_files() {
}
which_tracks() {
[ "$1" ] || usage "Filename expected."
[ "$1" ] || die "Filename expected."
list_tracked | while IFS=" " read repo file; do
[ "$file" = "$1" ] && echo "$repo"
done
@@ -169,32 +174,27 @@ clone_all() {
}
check_repo_name() {
[ "$1" ] || usage "Repo name expected."
[ "$1" ] || die "Repo name expected."
local name="$1"
name="${name//[^\.\-_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]/}"
[ "$name" = "$1" ] || \
usage "Invalid name: '$1'. Only letters, numbers, '.', '-' and '_' allowed."
[ "$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"
# check that the repo is not already cloned
[ -d ".mgit/$name" ] && {
echo "SKIPPING: Already exists: '$name'."
return 1
}
[ -d ".mgit/$name" ] && die "Already cloned: '$name'."
mkdir -p ".mgit/$name"
must mkdir -p ".mgit/$name"
export GIT_DIR=".mgit/$name/.git"
git init $MULTIGIT_INIT_OPTS
git config core.worktree ../../..
must git init $MULTIGIT_INIT_OPTS >&2
must git config core.worktree ../../..
git config core.excludesfile ".mgit/$name.exclude"
make_exclude_file "$name"
return 0
must git config core.excludesfile ".mgit/$name.exclude"
must make_exclude_file "$name"
}
make_exclude_file() {
@@ -212,8 +212,10 @@ clone_one() {
local url
local ver
[ "$VERBOSE" ] && say "CLONE: $1"
arg="${arg//[[:blank:]]/}" # spaces not allowed
[ "$arg" = "$1" ] || usage "Invalid name '$name'."
[ "$arg" = "$1" ] || die "Invalid name '$name'."
# extract `=version` if any
ver="${arg##*=}"
@@ -232,26 +234,25 @@ clone_one() {
[ "$origin" = "$arg" ] && origin=""
fi
[ "$VERBOSE" ] && echo "@$name:"
# check that arg is not `/` or `origin/`
[ "$name" ] || usage "Invalid repo name '$1'."
[ "$name" ] || die "Invalid repo name '$1'."
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")"
local ver1="$ver"
[ "$ver1" ] || ver1="$(git_ver_for "$name" long origin/master)"
[ "$ver1" = "$ver0" ] && return
echo "PULL: $name $ver (was: $ver0)"
export GIT_DIR=".mgit/$name/.git"
git fetch $MULTIGIT_FETCH_OPTS
[ "$ver1" = "$ver0" ] && exit 0
say "PULL: $name $ver (was: $ver0)"
must git fetch $MULTIGIT_FETCH_OPTS
if [ "$ver" ]; then
git -c advice.detachedHead=false checkout "$ver"
must git -c advice.detachedHead=false checkout "$ver"
else
git checkout -B master origin/master
must git checkout -B master origin/master
fi
export GIT_DIR=
return 0
exit 0
}
# check for a registered origin
@@ -261,13 +262,10 @@ clone_one() {
# decide the origin
if [ "$origin" ]; then
[ "$rorigin" -a "$origin" != "$rorigin" ] && \
echo "NOTE: Using different origin for '$name': '$origin' (was '$rorigin')."
say "NOTE: Using different origin for '$name': '$origin' (was '$rorigin')."
else
origin="$rorigin"
[ "$origin" ] || {
echo "SKIPPING: Unknown repo '$name'."
return
}
[ "$origin" ] || die "Unknown repo '$name'."
fi
# find the origin url
@@ -280,110 +278,111 @@ clone_one() {
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
say "ERROR: Unknown origin: '$origin' for '$name'."
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
no_exclude_file=1 init "$name" || return
git remote add origin "$url"
git fetch $MULTIGIT_FETCH_OPTS || {
(
no_exclude_file=1 must init "$name"
must git remote add origin "$url"
must git fetch $MULTIGIT_FETCH_OPTS
) || {
# cleanup on failed fetch because git doesn't.
rm -rf ".mgit/$name/"
rm ".mgit/$name.exclude"
echo "ERROR: Fetch failed. Repo removed."
return
rm -f ".mgit/$name.exclude"
die "Clone failed. Repo removed."
}
git branch --track master origin/master
git -c advice.detachedHead=false checkout $ver
must git branch --track master origin/master
must git -c advice.detachedHead=false checkout $ver
# make an exclude file if one wasn't checked out already.
make_exclude_file "$name"
must make_exclude_file "$name"
# (re)register the repo's origin.
if [ "$origin" != "$rorigin" ]; then
if [ "$rorigin" ]; then
echo "NOTE: Updating origin for '$name': $origin"
echo " (was: $rorigin)"
say "NOTE: Updating origin for '$name': $origin"
say " (was: $rorigin)"
else
echo "NOTE: Adding origin for '$name': $origin"
echo " (url: $url)"
say "NOTE: Adding origin for '$name': $origin"
say " (url: $url)"
fi
mkdir -p .mgit
echo "$origin" > ".mgit/$name.origin"
must mkdir -p .mgit
echo "$origin" > ".mgit/$name.origin" || die "Could not create file .mgit/$name.origin".
fi
}
clone() {
[ "$1" ] || usage "Repo name expected."
[ "$1" ] || die "Repo name expected."
if [ $# = 1 ]; then
clone_one "$@"
clone_one "$1"
else
while [ $# != 0 ]; do
clone_one "$1"
"$0" clone "$1"
shift
done
fi
}
run() { if [ "$DRY_RUN" ]; then echo "$@"; else "$@"; fi; }
remove_one() {
[ "$1" ] || usage "Invalid name '$1'."
[ "$1" ] || die "Invalid name '$1'."
[ -d ".mgit/$1/" ] || {
echo "ERROR: Repo not found '$1'."
say "ERROR: Repo not found '$1'."
return
}
# don't remove from a subshell
[ "$MULTIGIT_REPO" = "$1" ] && \
usage "Refusing to remove '$1' from a subshell."
die "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\`."
say "ERROR: 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\`."
return
}
# ask for confirmation if there are files to delete
[ "$files" -a "$YES" = "" ] && {
[ "$files" -a -z "$YES" ] && {
local n=$(echo "$files" | wc -l)
echo "Remove ALL $((n)) files of '$1'? You can't undo this [yes/N]"
say "Remove ALL $((n)) files of '$1'? You can't undo this [yes/N]"
read yes
[ "$yes" = "yes" ] || { echo "Canceled."; return; }
[ "$yes" = "yes" ] || { say "Canceled."; return; }
}
# remove files
for file in $files; do
run rm "$file"
dry 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
[ "$dir" != "." ] && dry /bin/rmdir -p "$dir" 2>/dev/null
done
# remove the git dir
run rm -rf ".mgit/$1/"
run rm -f ".mgit/$1.exclude"
dry rm -rf ".mgit/$1/"
dry rm -f ".mgit/$1.exclude"
echo "Removed: '$1'."
say "Removed: '$1'."
}
remove() {
[ "$1" ] || usage "Repo name expected."
[ "$1" ] || die "Repo name expected."
if [ $# = 1 ]; then
remove_one "$@"
remove_one "$1"
else
while [ $# != 0 ]; do
[ "$1" = "--yes" ] && { YES=1; shift; }
remove_one "$1"
shift
done
@@ -394,14 +393,14 @@ convert() {
local name="$1"
[ "$name" ] || name="$(basename "$PWD")"
check_repo_name "$name"
[ -d "$PWD0/.git" ] || usage "No .git dir in current dir"
[ -d ".mgit/$name" ] && usage "Repo already exists: $name."
mkdir -p ".mgit/$name"
mv "$PWD0/.git" ".mgit/$name/.git"
[ -d "$PWD0/.git" ] || die "No .git dir in current dir"
[ -d ".mgit/$name" ] && die "Repo already exists: $name."
must mkdir -p ".mgit/$name"
must mv "$PWD0/.git" ".mgit/$name/.git"
export GIT_DIR=".mgit/$name/.git"
git config core.worktree ../../..
git config core.excludesfile ".mgit/$name.exclude"
make_exclude_file "$name"
must git config core.worktree ../../..
must git config core.excludesfile ".mgit/$name.exclude"
must make_exclude_file "$name"
}
baseurl() {
@@ -418,13 +417,13 @@ baseurl() {
# 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'."
[ -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##*/}" != "" ] && usage "A base URL must end with a '/'."
[ "${url##*/}" != "" ] && die "A base URL must end with a '/'."
mkdir -p .mgit
echo "$url" > ".mgit/$origin.baseurl"
else
@@ -447,8 +446,8 @@ origin() {
# 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'."
[ -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"
@@ -469,7 +468,7 @@ list_releases() {
}
show_release() {
[ -f ".mgit/$1.release" ] || usage "Unknown release '$1'."
[ -f ".mgit/$1.release" ] || die "Unknown release '$1'."
cat ".mgit/$1.release"
}
@@ -504,7 +503,7 @@ list_release_repos() {
clone_release() {
local rel="$1"
[ "${rel##*.}" = "release" -a -f "$rel" ] || rel=".mgit/$rel.release"
[ -f "$rel" ] || usage "Release not found: '$1'."
[ -f "$rel" ] || die "Release not found: '$1'."
local s="$(cat "$rel")" # load it in memory to make sure we have it till the end.
@@ -514,22 +513,27 @@ clone_release() {
done
# step 2: clone/checkout repos present in the release.
echo "$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
"$0" clone "$repo=$ver"
fi
done)
echo "$s" | (
IFS=" "
local fail=0
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
"$0" clone "$repo=$ver" || fail=1
fi
done
exit $fail
)
}
release() {
local rel="$1"
local cmd="$2"
rel="${rel//[[:blank:]]/}" # spaces not allowed
[ "$rel" -a "$1" != "$rel" ] && usage "Invalid release name '$1'."
[ "$rel" -a "$1" != "$rel" ] && die "Invalid release name '$1'."
[ "$rel" ] || { list_releases; return; }
shift 2
case "$cmd" in
@@ -537,16 +541,16 @@ release() {
update) update_release "$rel" "$@" ;;
clone) clone_release "$rel" "$@" ;;
remove) remove_release "$rel" "$@" ;;
*) usage "Invalid release command '$cmd'."
*) die "Invalid release command '$cmd'."
esac
}
git_shell() {
cd "$PWD0" || return
echo "Entering subshell: git commands will affect the repo '$MULTIGIT_REPO'."
echo "Type \`exit' to exit subshell."
say "Entering subshell: git commands will affect the repo '$MULTIGIT_REPO'."
say "Type \`exit' to exit subshell."
git status -s
echo
say
if [ "$OSTYPE" = "msys" ]; then
export PROMPT="[$MULTIGIT_REPO] \$P\$G"
"$COMSPEC" /k
@@ -574,7 +578,7 @@ git_version() {
}
git_remove_links() {
[ "$OSTYPE" = "msys" ] && usage "Not for Windows."
[ "$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)
}
@@ -597,10 +601,10 @@ git_cmd_one() {
local repo="$1"
local cmd="$2"
shift 2
[ "$VERBOSE" ] && echo "@$repo:"
[ "$VERBOSE" ] && say "@$repo:"
export GIT_DIR="$PWD/.mgit/$repo/.git"
export MULTIGIT_REPO="$repo"
[ -d "$GIT_DIR" ] || usage "Unknown repo: '$repo'."
[ -d "$GIT_DIR" ] || die "Unknown repo: '$repo'."
case "$cmd" in
exec) cd "$PWD0" && "$@" ;;
ver) git_ver "$@" ;;
@@ -617,14 +621,12 @@ git_cmd_one() {
fi
;;
esac
export GIT_DIR=
export MULTIGIT_REPO=
}
git_cmd() {
local repos="$1"; shift
if [ "$repos" = "--all" ]; then
[ "$1" ] || usage "Refusing to start a subshell for each repo."
[ "$1" ] || die "Refusing to start a subshell for each repo."
for repo in `list_cloned`; do
git_cmd_one "$repo" "$@"
done
@@ -633,7 +635,7 @@ git_cmd() {
git_cmd_one "$repo" "$@"
done)
fi
[ "$1" = "ver" ] && echo
[ "$1" = "ver" ] && say
}
cd_root() {
@@ -641,7 +643,7 @@ cd_root() {
while [ "$PWD" != "$pwd1" ]; do
[ -d .mgit ] && return
pwd1="$PWD"
cd .. || usage "Could not cd to '$PWD/..'."
cd .. || die "Could not cd to '$PWD/..'."
done
cd "$PWD0" # root dir not found, go back to initial dir
}
@@ -651,11 +653,12 @@ cd_root
while true; do
case "$1" in
-v) export VERBOSE=1; shift ;;
--dry) export DRY_RUN=1; shift ;;
--yes) export YES=1; shift ;;
-SS) export GIT_SSH_COMMAND="ssh -o ControlMaster=auto -o ControlPersist=600 -o ControlPath=~/.ssh/sock-%r@%h-%p"; shift ;;
*) break ;;
-v) export VERBOSE=1; shift ;;
--debug) export DEBUG=1; shift ;;
--dry) export DRY=1; shift ;;
--yes) export YES=1; shift ;;
-SS) export GIT_SSH_COMMAND="ssh -o ControlMaster=auto -o ControlPersist=600 -o ControlPath=~/.ssh/sock-%r@%h-%p"; shift ;;
*) break ;;
esac
done
cmd="$1"; shift
@@ -683,6 +686,7 @@ case "$cmd" in
baseurl) baseurl "$@" ;;
origin) origin "$@" ;;
release) release "$@" ;;
bash) exec bash "$@" ;;
-) git_cmd "$@" ;;
*)
# look for and execute a plugin command