Skip to content

Instantly share code, notes, and snippets.

@martin-braun
Last active September 20, 2026 23:42
Show Gist options
  • Select an option

  • Save martin-braun/e10b7cafdd55feea44bc2d06d5c2c972 to your computer and use it in GitHub Desktop.

Select an option

Save martin-braun/e10b7cafdd55feea44bc2d06d5c2c972 to your computer and use it in GitHub Desktop.
vs - Git Vendor Sources - A POSIX compliant Unix tool to manage third-party git sources

Git Vendor Sources

Usage

Usage: vs [COMMAND] [REPO] [OPTIONS]
Adds, updates or deletes git source files of third party vendors in the local source storage at $HOME/.sources/.vendor.
This tool is meant to clone third party sources to compile or use without further modification.
It will try to checkout the latest tag and fallback to the latest branch, unless a prefered branch/tag is given.
If you want to load a workspace, consider forking and cloning using the normal git utility instead.

COMMAND
  ls                              Lists all locally added repositories/branches.
  dir                             Outputs the directory of the given repository/branch.
  sh                              Switches to the given repository/branch in a new shell.
  add                             Clones the given repository/branch with all its submodules with depth of 1 into the localized source storage and outputs its path.
  up                              Resets and pulls the latest changes on the given repository/branch. Will run ../.hooks/up.pre and ../.hooks/up.post.
  up-all                          Resets and pulls the latest changes on all repositories/branches. Will run ../.hooks/up.pre and ../.hooks/up.post.
  del                             Deletes the given repository/branch from the system. Will run ../.hooks/del.pre and ../.hooks/del.post.

REPO
  Unused on the ls and up-all command.
  For the add-command, needs to be a HTTPS URL or SSH destination ending with .git.
  For any other command it can also be just the sub path (without branch) on the file system.

OPTIONS
  -V,   --version                 Prints the version of this script.
  -v,   --verbose                 Prints verbose information.
  -p=*, --path=*                  Sets the working directory path (root) of the source storage (default: $HOME/.sources/.vendor).
  -b=*, --branch=*                Sets the branch/tag of the repo to work with. Tags lock to a specific version. The 'ls' command will accept -b=/ to inline branches into paths.
  -c=*, --cmd=*                   Automatically runs the given command expression instead of launching an interactive shell when using the 'sh' command.
  -f,   --force                   When using the up/up-all commands forces a hard reset if incoming changes cannot be rebased. When using the del command it ignores a missing repo and cleans up anyways.
  --aur                           REPO is a package name on the AUR and it will be expanded to the actual repo.
  --sh                            When adding a new repo, will automatically run the sh subcommand.
  -h,   --help                    Prints this help message.

Installation

The following commands will download the script and use it to install itself. It will also link the binary accordingly:

mkdir -p "$HOME/.local/bin"
ln -s "$(curl --proto '=https' --tlsv1.2 -sSf "https://gist.githubusercontent.com/martin-braun/e10b7cafdd55feea44bc2d06d5c2c972/raw/vs" | sh -s -- add "https://gist.github.com/e10b7cafdd55feea44bc2d06d5c2c972.git")/vs" "/usr/local/bin/vs"

Please make sure that $HOME/.local/bin is part of your PATH environment variable.

You can also install vs globally as root:

su
VS_PATH="/var/opt"
ln -s "$(curl --proto '=https' --tlsv1.2 -sSf "https://gist.githubusercontent.com/martin-braun/e10b7cafdd55feea44bc2d06d5c2c972/raw/vs" | sh -s -- add "https://gist.github.com/e10b7cafdd55feea44bc2d06d5c2c972.git" --path="$VS_PATH")/vs" "/usr/local/bin/vs"

Then you should alias vs on all users (i.e. in .bashrc) to point to use the global repository instead:

export VS_PATH="/var/opt"
alias vs='/usr/local/bin/vs --path="$VS_PATH"'

To pick a different path regardless of the alias, use \vs -p="/your/custom/path".

To confirm a successful installation, simply run vs ls. It should list this gist. By installing this tool with itself, it will also auto update itself when running vs up-all. If you ever install repositories to a custom path, you need to \vs -p="/your/custom/path" up-all as well.

NOTE: It's generally not adviced to incorparate this into server deployment because of potentially changing APIs. I recommend to look into ansible for that.

.agents
.copier-answers.yml
.opencode
.rsyncignore
AGENTS.md
CONTEXT.md
README.md
SPEC.md
dprint.json
opencode.json
# Created by https://www.toptal.com/developers/gitignore/api/windows,macos,linux,vim
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,linux,vim
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,vim
#!/usr/bin/env sh
# shellcheck disable=SC2065
version="0.2.0"
path="$HOME/.sources/.vendor"
usage="$(
cat << EOF
Usage: $(basename "$0") [COMMAND] [REPO] [OPTIONS]
Adds, updates or deletes git source files of third party vendors in the local source storage at $path.
This tool is meant to clone third party sources to compile or use without further modification.
It will try to checkout the latest tag and fallback to the latest branch, unless a prefered branch/tag is given.
If you want to load a workspace, consider forking and cloning using the normal git utility instead.
COMMAND
ls Lists all locally added repositories/branches.
dir Outputs the directory of the given repository/branch.
sh Switches to the given repository/branch in a new shell.
add Clones the given repository/branch with all its submodules with depth of 1 into the localized source storage and outputs its path.
up Resets and pulls the latest changes on the given repository/branch. Will run ../.hooks/up.pre and ../.hooks/up.post.
up-all Resets and pulls the latest changes on all repositories/branches. Will run ../.hooks/up.pre and ../.hooks/up.post.
audit Reviews net committed changes since the first unconfirmed upgrade using Git difftool. Keeps the pending baseline.
ok Explicitly clears the pending upgrade baseline for the given repository/branch.
del Deletes the given repository/branch from the system. Will run ../.hooks/del.pre and ../.hooks/del.post.
REPO
Unused on the ls and up-all command.
For the add-command, needs to be a HTTPS URL or SSH destination ending with .git.
For any other command it can also be just the sub path (without branch) on the file system.
OPTIONS
-V, --version Prints the version of this script.
-v, --verbose Prints verbose information.
-p=*, --path=* Sets the working directory path (root) of the source storage (default: $path).
-b=*, --branch=* Sets the branch/tag of the repo to work with. Tags lock to a specific version. The 'ls' command will accept -b=/ to inline branches into paths.
For up/audit/ok, omit for the rolling baseline; use -b BRANCH or REPO/BRANCH for a branch baseline.
-c=*, --cmd=* Automatically runs the given command expression instead of launching an interactive shell when using the 'sh' command.
-f, --force When using the up/up-all commands forces a hard reset if incoming changes cannot be rebased. When using the del command it ignores a missing repo and cleans up anyways.
--aur REPO is a package name on the AUR and it will be expanded to the actual repo.
--sh When adding a new repo, will automatically run the sh subcommand.
-h, --help Prints this help message.
EOF
)"
c_red="$(printf '\033[31m')"
c_yellow="$(printf '\033[33m')"
c_green="$(printf '\033[32m')"
c_reset="$(printf '\033[0m')"
while [ "$#" -gt 0 ]; do
case "$1" in
-V | --version) echo "$version" && exit 0 ;;
-v | --verbose) verbose=1 ;;
-p | --path) shift; path="$1" ;;
-p=* | --path=*) path="${1#*=}" ;;
-b | --branch) shift; branch="$1" ;;
-b=* | --branch=*) branch="${1#*=}" ;;
-c | --cmd) shift; cmd="$1" ;;
-c=* | --cmd=*) cmd="${1#*=}" ;;
-f | --force) force=1 ;;
--aur) aur=1 ;;
--sh) sh=1 ;;
-h | --help) echo "$usage" >&2 && exit 129 ;;
*)
if [ "$command" = "" ]; then
command="$1"
elif [ "$repo" = "" ]; then
repo="$1"
else
echo "Invalid command line flag $1." >&2
exit 1
fi
;;
esac
shift
done
test ! "$aur" = 1 || repo="https://aur.archlinux.org/$repo.git"
lockedbranch="$branch"
test -n "$verbose" && set -x
set -e
command -v "git" > /dev/null 2>&1 || {
echo "Missing git." >&2
exit 1
}
test ! "$command" = "" || {
echo "Missing command." >&2
exit 1
}
linksubpath="share"
if [ ! -d "$path/$linksubpath" ]; then
mkdir -p "$path"
chown -R "$(whoami)" "$path"
mkdir -p "$path/$linksubpath"
fi
if [ "$command" = "ls" ]; then
if [ "$branch" = "/" ]; then
find "$path/$linksubpath" -maxdepth 1 -type l -exec basename {} \; | sed 's/,,/\//' | tr , /
find "$path/$linksubpath" -maxdepth 1 -type f -name '*.lnk' -exec basename {} \; | sed 's/^\.\///' | sed 's/,,/\//' | sed 's/\.lnk$//' | tr , / # lnk support for network sync to msys2
else
find "$path/$linksubpath" -maxdepth 1 -type l -exec basename {} \; | sed 's/,,/ -b=/' | tr , /
find "$path/$linksubpath" -maxdepth 1 -type f -name '*.lnk' -exec basename {} \; | sed 's/^\.\///' | sed 's/,,/ -b=/' | sed 's/\.lnk$//' | tr , / # lnk support for network sync to msys2
fi
exit
elif [ "$command" = "up-all" ]; then
extra_flags=""
if [ -n "$force" ]; then
extra_flags="--force"
fi
if [ -n "$verbose" ]; then
extra_flags="$extra_flags --verbose"
fi
# shellcheck disable=SC2086
"$0" -p="$path" ls | xargs -r -L1 "$0" $extra_flags -p="$path" up
exit
fi
test -n "$repo" || {
echo "Missing repo." >&2
exit 1
}
test "$repo" = "${repo% *}" || {
echo "Cannot have spaces in repo." >&2
exit 1
}
reposubpath="$repo"
reposubpath="${reposubpath#*//}" # remove ^https://
reposubpath="${reposubpath#*@}" # remove ^git@
reposubpath="${reposubpath%.git}" # remove .git$
reposubpath=$(printf '%s' "$reposubpath" | tr : /) # replace : with /
test ! "$(printf '%s' "$reposubpath" | tr -d / | tr -d '.')" = "" || {
echo "Local reposubpath results in empty or invalid path." >&2
exit 1
}
if [ "$branch" = "" ]; then
reposubpathslashes=$(echo "$reposubpath" | tr -cd '/')
if [ "${#reposubpathslashes}" = "3" ]; then # path has branch/tag, so remove it from reposubpath and set it in branch
branch="${reposubpath##*/}"
reposubpath="${reposubpath%/*}"
fi
fi
# only when the branch is explicitely defined, we remember it.
if [ "$branch" = "" ]; then
link="$path/$linksubpath/$(printf '%s' "$reposubpath" | tr / ,)" # no branch/tag for rolling release
else
link="$path/$linksubpath/$(printf "%s,,%s" "$reposubpath" "$branch" | tr / ,)"
fi
reviewbranch="${branch:-_}"
if [ "$command" = "audit" ] || [ "$command" = "ok" ]; then
git check-ref-format "refs/heads/$reviewbranch" > /dev/null 2>&1 || {
echo "Invalid audit branch $reviewbranch." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
fullauditpath="${path:?}/$reposubpath/.audit"
# Encode percent first: slash branches stay inside .audit without escape collisions.
reviewname="$(printf '%s' "$reviewbranch" | sed 's/%/%25/g; s,/,%2F,g')" || {
rc=$?
echo "Cannot encode audit branch $reviewbranch." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
auditfile="$fullauditpath/$reviewname.commit"
test ! -L "$fullauditpath" && test ! -L "$auditfile" || {
echo "Pending audit paths must not be symbolic links." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
if [ "$command" = "audit" ]; then
current_checkout="$("$0" -p="$path" dir "$reposubpath" -b="$branch")" || {
rc=$?
echo "Cannot resolve the current checkout for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
fi
test -f "$auditfile" || {
echo "Nothing pending to $command for $repo ($reviewbranch)." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 2
}
if [ "$command" = "ok" ]; then
rm -f -- "$auditfile" || {
rc=$?
echo "Cannot confirm pending upgrades for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
exit
fi
oldcommit="$(cat "$auditfile")" || {
rc=$?
echo "Cannot read pending baseline $auditfile." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
case "$oldcommit" in
'' | *[!0-9a-fA-F]*)
echo "Invalid pending commit in $auditfile." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
;;
esac
currentcommit="$(git -C "$current_checkout" rev-parse --verify 'HEAD^{commit}')" || {
rc=$?
echo "Cannot resolve the current HEAD for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
origin="$(git -C "$current_checkout" remote get-url origin)" || {
rc=$?
echo "Cannot resolve origin for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
test -n "$origin" || {
echo "Empty origin URL for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
# Local relative URLs are relative to the checkout, not the temporary repository.
case "$origin" in
/* | *://*) ;;
*)
case "${origin%%/*}" in
*:*) ;;
*) origin="$current_checkout/$origin" ;;
esac
;;
esac
objectformat="$(git -C "$current_checkout" rev-parse --show-object-format)" || {
rc=$?
echo "Cannot resolve the Git object format for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
reviewtmp=""
trap 'rc=$?; trap - 0; test -z "$reviewtmp" || rm -rf -- "$reviewtmp" || { test "$rc" -ne 0 || rc=1; }; exit "$rc"' 0
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
reviewtmp="$(mktemp -d)" || {
rc=$?
echo "Cannot create a temporary audit directory." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
git init --bare --object-format="$objectformat" "$reviewtmp/repo" >&2 || {
rc=$?
echo "Cannot initialize the temporary audit repository." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
git --git-dir="$reviewtmp/repo" fetch --depth 1 --no-tags --no-recurse-submodules --no-write-fetch-head -- "$origin" "$oldcommit:refs/audit/old" "$currentcommit:refs/audit/current" || {
rc=$?
echo "Cannot fetch audit commits $oldcommit and $currentcommit from origin; objects may no longer be reachable. Pending baseline retained." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
# Preserve effective tool preferences, including checkout-local and conditional config.
rc=0
toolkeys="$(git -C "$current_checkout" config --name-only --get-regexp '^(diff\.(tool|guitool)|difftool\..*|merge\.(tool|guitool)|mergetool\..*)$')" || rc=$?
test "$rc" -eq 0 || test "$rc" -eq 1 || {
echo "Cannot read Git difftool settings for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
if [ -n "$toolkeys" ]; then
printf '%s\n' "$toolkeys" | while IFS= read -r toolkey; do
toolvalue="$(git -C "$current_checkout" config --get "$toolkey")" || {
rc=$?
echo "Cannot read Git setting $toolkey." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
git --git-dir="$reviewtmp/repo" config "$toolkey" "$toolvalue" || {
rc=$?
echo "Cannot configure the temporary difftool." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
done || {
rc=$?
echo "Cannot copy Git difftool settings for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
fi
# Difftool requires a work tree even when comparing two committed trees.
mkdir "$reviewtmp/worktree" || {
rc=$?
echo "Cannot create the temporary difftool work tree." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
git --git-dir="$reviewtmp/repo" --work-tree="$reviewtmp/worktree" difftool --trust-exit-code --submodule=short refs/audit/old refs/audit/current -- || {
rc=$?
echo "Git difftool failed for $repo; pending baseline retained." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
exit
fi
if [ "$command" = "dir" ]; then
if [ -f "$link.lnk" ]; then
# Ugly msys2 work around for VMs with network storage, i.e. Parallels turns symlinks into lnk files.
# Just parse the name, but the branch needs to derived from the default branch, if it has not been supplied.
# This will break things if the default branch changes, which will happen for tagged releases.
# TODO: Probably also create a database file for each repo, which points to the right branch.
if [ "$branch" = "" ]; then
branch="$(git ls-remote --tags "https://$reposubpath.git" | sort -t v -k2,2V | tail -n1 | awk -F '[/\\^]' '{print $3}')" # get latest tag
test ! "$branch" = "" || branch="$(git ls-remote --symref "https://$reposubpath.git" HEAD | awk '/^ref:/ {sub(/refs\/heads\//, "", $2); print $2}')" # or get default branch
test ! "$branch" = "" || {
echo "Repository $repo doesn't exist or it has no tags or no default branch." >&2
exit 3
}
fi
echo "$path/$(basename "$link.lnk" | sed 's/^\.\///' | sed 's/,,/ -b=/' | sed 's/\.lnk$//' | tr , /)/$branch" && exit # lnk support for network sync to msys2
fi
test -h "$link" || {
echo /dev/null
echo "Repository $repo doesn't exist on the local system, because $link doesn't exist." >&2
exit 2
}
readlink -f "$link"
exit
elif [ "$command" = "sh" ]; then
(
cd "$("$0" -p="$path" dir "$repo" -b="$lockedbranch")"
{ test "$cmd" = "" && "$SHELL" -i; } || "$SHELL" -c "$cmd"
)
exit
fi
if [ "$branch" = "" ]; then
branch="$(git ls-remote --tags "https://$reposubpath.git" | sort -t v -k2,2V | tail -n1 | awk -F '[/\\^]' '{print $3}')" # get latest tag
test ! "$branch" = "" || branch="$(git ls-remote --symref "https://$reposubpath.git" HEAD | awk '/^ref:/ {sub(/refs\/heads\//, "", $2); print $2}')" # or get default branch
test ! "$branch" = "" || branch="master"
echo "Autopicked branch $branch" >&2
fi
fullrepopath="$path/$reposubpath/$branch"
echo "Full path to repository: $fullrepopath" >&2
fullhookspath="${path:?}/$reposubpath/.hooks"
echo "Full path to its hooks: $fullhookspath" >&2
if [ "$command" = "add" ]; then
if [ -L "$link" ] || [ -d "$fullrepopath" ]; then
echo "Repository $repo already added to the local system." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 2
fi
echo "Adding $repo ($branch) to $fullrepopath…" >&2
mkdir -p "$fullrepopath" >&2
echo "Cloning $repo ($branch) to $fullrepopath…" >&2
if ! git clone --recursive --depth 1 --branch "$branch" "$repo" "$fullrepopath"; then
rm -rf "$fullrepopath"
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 3
fi
ln -sf "../$reposubpath/$branch" "$link" >&2
echo "Linked $fullrepopath to $link." >&2
test -h "$link" || {
echo /dev/null
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 4
}
readlink -f "$link"
mkdir -p "$fullhookspath" >&2 # makes it easier to find by the user
rc=0
if [ "$sh" = 1 ]; then
if [ -n "$cmd" ]; then
"$0" -p="$path" sh "$repo" -b="$lockedbranch" -c="$cmd" || rc=$?
else
"$0" -p="$path" sh "$repo" -b="$lockedbranch" || rc=$?
fi
fi
if [ ! "$rc" -eq 0 ]; then
printf '%s%-10s%s %s\n' "$c_yellow" "INCOMPLETE" "$c_reset" "$repo" >&2
exit $rc
fi
printf '%s%-10s%s %s\n' "$c_green" "ADDED" "$c_reset" "$repo" >&2
elif [ "$command" = "up" ]; then
test -L "$link" || {
echo "Repository $repo doesn't exist on the local system." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 2
}
oldfullrepopath="$(realpath "${path:?}/$linksubpath/$(readlink "$link")")"
test -d "$oldfullrepopath" || {
echo "Repository $repo doesn't exist on the local system, because $oldfullrepopath does not exist." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 2
}
echo "Updating $oldfullrepopath…" >&2
if [ "$oldfullrepopath" = "$fullrepopath" ]; then
# same branch
rc=0
git -C "$fullrepopath" fetch --depth 1 origin "$branch" >&2 || rc=$?
if [ ! "$rc" -eq 0 ]; then
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
fi
if [ -n "$(git -C "$fullrepopath" diff HEAD "origin/$branch" 2> /dev/null || exit 0)" ]; then
# has new changes on remote
(
oldcommit="$(git -C "$oldfullrepopath" rev-parse --verify 'HEAD^{commit}')" || {
rc=$?
echo "Cannot resolve the pre-upgrade HEAD for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
git check-ref-format "refs/heads/$reviewbranch" > /dev/null 2>&1 || {
echo "Invalid audit branch $reviewbranch." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
fullauditpath="${path:?}/$reposubpath/.audit"
# Encode percent first: slash branches stay inside .audit without escape collisions.
reviewname="$(printf '%s' "$reviewbranch" | sed 's/%/%25/g; s,/,%2F,g')" || {
rc=$?
echo "Cannot encode audit branch $reviewbranch." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
auditfile="$fullauditpath/$reviewname.commit"
test ! -L "$fullauditpath" && test ! -L "$auditfile" || {
echo "Pending audit paths must not be symbolic links." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
mkdir -p "$fullauditpath" || {
rc=$?
echo "Cannot create $fullauditpath." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
test ! -f "$auditfile" || exit 0
auditpending=""
trap 'rc=$?; trap - 0; test -z "$auditpending" || rm -f -- "$auditpending" || { test "$rc" -ne 0 || rc=1; }; exit "$rc"' 0
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
auditpending="$(mktemp "$fullauditpath/.pending.XXXXXX")" || {
rc=$?
echo "Cannot create a pending baseline file." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
printf '%s\n' "$oldcommit" > "$auditpending" || {
rc=$?
echo "Cannot write pending audit baseline for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
# Publish complete bytes atomically; a competing creator keeps its oldest baseline.
ln -T -- "$auditpending" "$auditfile" || {
test -f "$auditfile" && test ! -L "$auditfile" || {
echo "Cannot save pending audit baseline $auditfile." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
}
)
test -x "$fullhookspath/up.pre" && "$fullhookspath/up.pre" || true
if ! git -C "$fullrepopath" pull --rebase; then
git -C "$fullrepopath" rebase --abort 2>/dev/null || true
if [ "$force" ]; then
git -C "$fullrepopath" reset --hard
git -C "$fullrepopath" pull --ff-only
else
echo "Update failed, because incoming changes cannot be merged with local changes. Use the -f flag to force a reset of local changes or manually rebase and solve the conflicts." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 3
fi
fi
test -x "$fullhookspath/up.post" && "$fullhookspath/up.post" || true
printf '%s%-10s%s %s\n' "$c_yellow" "UPGRADED" "$c_reset" "$repo" >&2
exit
fi
printf '%s%-10s%s %s\n' "$c_green" "SKIPPED" "$c_reset" "$repo" >&2
else
# new version/tag/branch
if [ -d "$fullrepopath" ]; then
# the new version already exists, because there is also a locked pull
echo "The version you want to upgrade to is already locked in a separate pull." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 3
fi
(
oldcommit="$(git -C "$oldfullrepopath" rev-parse --verify 'HEAD^{commit}')" || {
rc=$?
echo "Cannot resolve the pre-upgrade HEAD for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
git check-ref-format "refs/heads/$reviewbranch" > /dev/null 2>&1 || {
echo "Invalid audit branch $reviewbranch." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
fullauditpath="${path:?}/$reposubpath/.audit"
# Encode percent first: slash branches stay inside .audit without escape collisions.
reviewname="$(printf '%s' "$reviewbranch" | sed 's/%/%25/g; s,/,%2F,g')" || {
rc=$?
echo "Cannot encode audit branch $reviewbranch." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
auditfile="$fullauditpath/$reviewname.commit"
test ! -L "$fullauditpath" && test ! -L "$auditfile" || {
echo "Pending audit paths must not be symbolic links." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
mkdir -p "$fullauditpath" || {
rc=$?
echo "Cannot create $fullauditpath." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
test ! -f "$auditfile" || exit 0
auditpending=""
trap 'rc=$?; trap - 0; test -z "$auditpending" || rm -f -- "$auditpending" || { test "$rc" -ne 0 || rc=1; }; exit "$rc"' 0
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
auditpending="$(mktemp "$fullauditpath/.pending.XXXXXX")" || {
rc=$?
echo "Cannot create a pending baseline file." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
printf '%s\n' "$oldcommit" > "$auditpending" || {
rc=$?
echo "Cannot write pending audit baseline for $repo." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit $rc
}
# Publish complete bytes atomically; a competing creator keeps its oldest baseline.
ln -T -- "$auditpending" "$auditfile" || {
test -f "$auditfile" && test ! -L "$auditfile" || {
echo "Cannot save pending audit baseline $auditfile." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 1
}
}
)
test -x "$fullhookspath/up.pre" && "$fullhookspath/up.pre" || true
mkdir -p "$fullrepopath" >&2
if ! git clone --recursive --depth 1 --branch "$branch" "https://$reposubpath.git" "$fullrepopath"; then
rm -rf "$fullrepopath"
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 3
fi
ln -sf "../$reposubpath/$branch" "$link" >&2
test -x "$fullhookspath/up.post" && "$fullhookspath/up.post" || true
rm -rf "$oldfullrepopath" >&2 || {
echo "Failed to remove old version." >&2
printf '%s%-10s%s %s\n' "$c_yellow" "INCOMPLETE" "$c_reset" "$repo" >&2
exit 4
}
printf '%s%-10s%s %s\n' "$c_yellow" "UPGRADED" "$c_reset" "$repo" >&2
fi
elif [ "$command" = "del" ]; then
test -d "$link" || test -n "$force" || {
echo "Repository $repo doesn't exist on the local system. Use the -f flag to forcefully try delete and clean up anyways." >&2
printf '%s%-10s%s %s\n' "$c_red" "ERROR" "$c_reset" "$repo" >&2
exit 2
}
test -d "$link" && fullrepopath="$path$(readlink "$link" | sed 's/^..//')" # get real fullrepopath in case of older rolling
echo "Deleting $fullrepopath…" >&2
test -x "$fullhookspath/del.pre" && "$fullhookspath/del.pre" || true
test -z "$fullrepopath" || rm -rf "$fullrepopath" >&2 || test -n "$force"
echo "Cleaning up…" >&2
rm -rf "$link" || test -n "$force" >&2
rmdir "${path:?}/$linksubpath" 2> /dev/null || true
test -x "$fullhookspath/del.post" && "$fullhookspath/del.post" || true
find "${path:?}/" -depth -mindepth 1 -maxdepth 2 -type d -exec rmdir {} + 2> /dev/null || true
printf '%s%-10s%s %s\n' "$c_green" "DELETED" "$c_reset" "$repo" >&2
else
echo "Unknown command $command." >&2
exit 1
fi
set +e && set +x
_vs() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ ${cur} == --* ]] ; then
COMPREPLY=( $(compgen -W "--version --verbose --path= --branch= --cmd= --force --aur --sh --help" -- ${cur}) )
return 0
fi
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "-V -v -p= -b= -c= -f -h" -- ${cur}) )
return 0
fi
if [[ ${#COMP_WORDS[@]} == 2 ]] ; then
COMPREPLY=( $(compgen -W "ls dir sh add up up-all audit ok del" -- ${cur}) )
return 0
fi
if [[ ${prev} == "dir" || ${prev} == "sh" || ${prev} == "up" || ${prev} == "audit" || ${prev} == "ok" || ${prev} == "del" ]] ; then
COMPREPLY=( $(compgen -W "$(vs ls -b=/ | xargs echo)" -- ${cur}) )
return 0
fi
}
complete -o nospace -F _vs vs
# vi: ft=bash
@martin-braun

martin-braun commented Jan 18, 2024 •

Copy link
Copy Markdown
Author

// reserved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment