-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·166 lines (150 loc) · 6.82 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·166 lines (150 loc) · 6.82 KB
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env sh
# install.sh — put profullstack/scripts on PATH, at a released version.
#
# sh install.sh # latest release tag
# sh install.sh --edge # track main instead
# sh install.sh --ref v0.2.0 # a specific tag or branch
# sh install.sh --ssh # clone over SSH, for a checkout you push from
# sh install.sh --dry-run # say what would happen, change nothing
#
# The repo is public, so a fresh box needs no key and no account:
#
# curl -fsSL https://raw.githubusercontent.com/profullstack/scripts/main/install.sh | sh
#
# That is the whole install. Cloning over HTTPS also ends the chicken-and-egg
# this file used to carry: provision-ssh-keys lives in this repo, so getting it
# onto a new machine used to need the very key it exists to install.
#
# Use --ssh (or SCRIPTS_REPO) for a checkout you intend to push from. An HTTPS
# clone reads fine and upgrades fine; it is just not set up to write.
#
# POSIX sh on purpose. This is the one file that runs before anything is
# installed, possibly on a box whose only shell is dash, so it may not assume
# bash the way the tools in bin/ do.
#
# THE GATE RUNS TWICE, ON PURPOSE. bin/scripts-check is the rule. This script
# points core.hooksPath at .githooks so it runs before every push, and
# .github/workflows/release.yml runs that same file on the runner.
#
# CI is back because the repo is public. An unpaid balance on the org suspends
# Actions compute on PRIVATE repos -- not this repo's minutes, which are free;
# storage and Code Quality credits elsewhere -- and while this repo was private
# the job was refused before it ever started. The balance is still owed; public
# repos are simply not subject to it.
#
# WHY A TAG BY DEFAULT. Because `~/.local/bin/*` are symlinks into the working
# tree, the command on PATH is whatever the checkout happens to be at. Tracking
# main means a half-finished edit made at a prompt is instantly the installed
# tool on that box. Pinning to the newest release makes an upgrade a decision.
# The development box is the exception, and it takes care of itself: an
# existing checkout with local commits or uncommitted work is never moved.
set -eu
# HTTPS by default: the repo is public, so this works with no key, no account
# and no prior setup, which is the whole point of the curl one-liner above.
# SSH stays a flag away for the boxes that push.
REPO_HTTPS="https://github.com/profullstack/scripts.git"
REPO_SSH="git@github.com:profullstack/scripts.git"
REPO="${SCRIPTS_REPO:-$REPO_HTTPS}"
DIR="${SCRIPTS_DIR:-$HOME/scripts}"
BINDIR="${SCRIPTS_BIN:-$HOME/.local/bin}"
REF="${SCRIPTS_REF:-}"
DRY=0
while [ $# -gt 0 ]; do
case "$1" in
--edge) REF=main ;;
--ssh) REPO="$REPO_SSH" ;;
--https) REPO="$REPO_HTTPS" ;;
--ref) REF="${2:?--ref needs a tag or branch}"; shift ;;
--ref=*) REF="${1#--ref=}" ;;
--dir) DIR="${2:?--dir needs a path}"; shift ;;
--bin) BINDIR="${2:?--bin needs a path}"; shift ;;
--dry-run|-n) DRY=1 ;;
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "install.sh: unknown option $1" >&2; exit 2 ;;
esac
shift
done
say() { echo "$@"; }
run() { if [ "$DRY" = 1 ]; then echo " would: $*"; else "$@"; fi; }
die() { echo "install.sh: $*" >&2; exit 1; }
# The newest v-tag on the remote, by version sort. Read from the remote rather
# than a local tag list so a stale checkout cannot report an old "latest".
latest_tag() {
git ls-remote --tags --refs "$REPO" 'v*' 2>/dev/null \
| awk '{print $2}' | sed 's#refs/tags/##' \
| sort -V | tail -1
}
# ---------------------------------------------------------------- fetch
if [ ! -d "$DIR/.git" ]; then
[ -e "$DIR" ] && die "$DIR exists but is not a git checkout; move it aside"
if [ -z "$REF" ]; then REF="$(latest_tag)"; [ -n "$REF" ] || REF=main; fi
say "cloning $REPO -> $DIR at $REF"
run git clone --quiet "$REPO" "$DIR"
[ "$DRY" = 1 ] || git -C "$DIR" -c advice.detachedHead=false checkout --quiet "$REF"
else
say "checkout exists at $DIR"
# Refuse to move a tree someone is working in. On the development box this
# is the normal state, and silently detaching it onto a tag would strand
# unpushed work in a way that only surfaces later.
dirty=$(git -C "$DIR" status --porcelain 2>/dev/null | head -1)
branch=$(git -C "$DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)
ahead=0
if [ "$branch" != HEAD ] && git -C "$DIR" rev-parse --verify --quiet "@{u}" >/dev/null 2>&1; then
ahead=$(git -C "$DIR" rev-list --count "@{u}..HEAD" 2>/dev/null || echo 0)
fi
if [ -n "$dirty" ] || [ "$ahead" != 0 ]; then
say " leaving the tree alone: $( [ -n "$dirty" ] && echo 'uncommitted changes' || echo "$ahead unpushed commit(s)" )"
say " (symlinks below still refresh, so a new tool in bin/ lands on PATH)"
else
run git -C "$DIR" fetch --quiet --tags origin
if [ -z "$REF" ]; then REF="$(latest_tag)"; [ -n "$REF" ] || REF=main; fi
say " moving to $REF"
if [ "$REF" = main ]; then
run git -C "$DIR" checkout --quiet main
run git -C "$DIR" merge --quiet --ff-only origin/main
else
run git -C "$DIR" -c advice.detachedHead=false checkout --quiet "$REF"
fi
fi
fi
# ---------------------------------------------------------------- hooks
# The release gate is a pre-push hook rather than CI. Hooks are not carried by
# a clone, so every checkout has to be pointed at the tracked directory; it is
# a local config setting, which is why it is done here and not once in the repo.
if [ "$DRY" = 1 ]; then
echo " would: git -C $DIR config core.hooksPath .githooks"
elif [ -d "$DIR/.githooks" ]; then
git -C "$DIR" config core.hooksPath .githooks
say " pre-push gate enabled (scripts-check)"
fi
# ---------------------------------------------------------------- link
run mkdir -p "$BINDIR"
linked=0; skipped=0
for f in "$DIR"/bin/*; do
[ -f "$f" ] && [ -x "$f" ] || continue
name=$(basename "$f")
target="$BINDIR/$name"
# Replace our own symlink freely; never overwrite a real file someone else
# put there. A silent clobber of another tool of the same name is the kind
# of thing found weeks later.
if [ -e "$target" ] && [ ! -L "$target" ]; then
say " skip $name (a real file, not our symlink)"
skipped=$((skipped + 1))
continue
fi
run ln -sfn "$f" "$target"
linked=$((linked + 1))
done
say "linked $linked tool(s) into $BINDIR${skipped:+, skipped $skipped}"
# ---------------------------------------------------------------- report
case ":${PATH}:" in
*":$BINDIR:"*) ;;
*) say ""
say "NOTE: $BINDIR is not on PATH. Add it:"
say " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.zshenv" ;;
esac
if [ "$DRY" = 0 ]; then
at=$(git -C "$DIR" describe --tags --always 2>/dev/null || echo unknown)
say ""
say "installed at $at — \`scripts-upgrade\` to move to the next release"
fi