#!/bin/bash # desktop-assistant tool: clone a public git repository into the project tree. # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # clone.sh [to] [update]: clone into //- (just # when the URL has no owner, or when owner and repo are the same), the # folder chosen by host from the clone-hosts map (github.com -> GITHUB, ...). An # unknown host stops and says which folder to offer; passing that folder as # clones there and remembers the mapping. =1 pulls an existing # clone instead. Check: test-clone.sh. set -uo pipefail # A missing or private repo must fail, not wait on a login: git falls back to # SSH_ASKPASS (a pinentry dialog on this desktop) and then the terminal. unset SSH_ASKPASS export GIT_TERMINAL_PROMPT=0 url=${1:-} to=${2:-} update=${3:-} root=${CLONE_ROOT:-$HOME/Programming/GIT} map=${CLONE_MAP:-${XDG_CONFIG_HOME:-$HOME/.config}/desktop-assistant/clone-hosts} [ -n "$url" ] || { echo "url is empty" >&2; exit 1; } # The fragment and query are not part of a repository; a trailing slash is not # either. The repo name is the last path segment with a .git suffix removed. url=${url%%\#*} url=${url%%\?*} url=${url%/} case "$url" in *://*) rest=${url#*://} rest=${rest#*@} host=${rest%%/*} path=${rest#*/} ;; *@*:*) rest=${url#*@} host=${rest%%:*} path=${rest#*:} ;; *) echo "not a git URL: $url" >&2; exit 1 ;; esac host=${host%%:*} host=${host,,} path=${path%/} repo=${path##*/} repo=${repo%.git} # The segment before the repo is the owner; a nested group keeps only its last # segment. No slash at all means no owner. if [ "${path%/*}" = "$path" ]; then owner="" else owner=${path%/*} owner=${owner##*/} fi # Every name lands in a path, so none may escape the tree. [[ $host =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]] || { echo "bad host: $host" >&2; exit 1; } [[ $repo =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || { echo "bad repository name: $repo" >&2; exit 1; } [ -z "$owner" ] || [[ $owner =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || { echo "bad owner name: $owner" >&2; exit 1; } # host -> folder, one tab-separated pair per line, seeded once. Appending the # approved folder is how a new host is remembered, so the next clone of that # host goes straight there. if [ ! -e "$map" ]; then mkdir -p "$(dirname "$map")" || exit 1 printf '%s\t%s\n' github.com GITHUB gitlab.com GITLAB \ forge.slackware.nl FORGE > "$map" || exit 1 fi folder=$(awk -v h="$host" '$1 == h { f = $2 } END { print f }' "$map") if [ -n "$to" ]; then [[ $to =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || { echo "bad folder name: $to" >&2; exit 1; } [ -n "$folder" ] || printf '%s\t%s\n' "$host" "$to" >> "$map" || exit 1 folder=$to fi name=$repo [ -z "$owner" ] || [ "$owner" = "$repo" ] || name=$owner-$repo if [ -z "$folder" ]; then label=${host%.*} label=$(printf '%s' "$label" | tr '[:lower:]' '[:upper:]') echo "no folder for host $host; ask the user: create $label/ and clone $name there? if yes, call again with to=$label" exit 0 fi target=$root/$folder/$name # Never clone over an existing folder, and never under another name: offer an # update instead. ff-only so a diverged clone is reported, not merged. if [ -e "$target" ]; then if [ "$update" = 1 ]; then out=$(git -C "$target" pull --ff-only 2>&1) && { echo "updated $target: $(tail -1 <<< "$out")" exit 0 } echo "could not update $target: $(tail -1 <<< "$out")" >&2 exit 1 fi echo "already cloned at $target; ask the user: update it? if yes, call again with update=1" exit 0 fi mkdir -p "$root/$folder" || exit 1 if out=$(git clone -- "$url" "$target" 2>&1); then echo "cloned $url into $target" else echo "clone failed: $(tail -1 <<< "$out")" >&2 exit 1 fi