aboutsummaryrefslogtreecommitdiffstats
path: root/examples/tools/clone.sh
blob: 2f9defce7fe9ef2ed11d4497df4a0b439c4a79f5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/bin/bash
# desktop-assistant tool: clone a public git repository into the project tree.
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
#
# 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 <url> [to] [update]: clone into <root>/<folder>/<owner>-<repo> (just
# <repo> 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
# <to> clones there and remembers the mapping. <update>=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