Skip to content

Commit fce6191

Browse files
committed
Create a shell script for fetching and installing the 'latest' or any specific release tag for ms-git.
1 parent c0d7923 commit fce6191

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed

contrib/install-git-mac.sh

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
#!/usr/bin/env zsh
2+
3+
# install-git (macOS): Download a Git installer from https://github.com/microsoft/git/releases
4+
#
5+
# macOS-only: This script targets mac (.pkg) assets only, and does not support other platforms.
6+
7+
set -e
8+
set -u
9+
set -o pipefail
10+
11+
# --- defaults ---
12+
DO_INSTALL=true # install by default
13+
LIST_ONLY=false
14+
OUT="" # empty -> ~/Downloads/<asset_name>
15+
QUIET=false
16+
REPO="microsoft/git"
17+
VERSION="latest"
18+
19+
# --- tool paths (always use absolute paths) ---
20+
AWK="/usr/bin/awk"
21+
CURL="/usr/bin/curl"
22+
GREP="/usr/bin/grep"
23+
HEAD="/usr/bin/head"
24+
INSTALLER="/usr/sbin/installer"
25+
JQ="/usr/bin/jq"
26+
MKDIR="/bin/mkdir"
27+
RM="/bin/rm"
28+
RMDIR="/bin/rmdir"
29+
SUDO="/usr/bin/sudo"
30+
UNAME="/usr/bin/uname"
31+
32+
err() { printf "[error] %s\n" "$*" 1>&2; }
33+
die() { err "$*"; exit 1; }
34+
print_q() { $QUIET || printf "%s\n" "$*"; }
35+
36+
# Extract asset name and URL pairs as tab-separated values from provided JSON.
37+
# Arg1: JSON string
38+
# Arg2: with-sources | no-sources (include tarball/zip pseudo-assets)
39+
extract_assets_from() {
40+
local _json="$1" _with_src="${2:-with-sources}"
41+
printf "%s" "$_json" | "$JQ" -r '.assets[] | [.name, .browser_download_url] | @tsv'
42+
if [[ "$_with_src" == "with-sources" ]]; then
43+
local tb zb
44+
tb=$(printf "%s" "$_json" | "$JQ" -r '.tarball_url // empty')
45+
zb=$(printf "%s" "$_json" | "$JQ" -r '.zipball_url // empty')
46+
[[ -n "$tb" ]] && printf "source.tar.gz\t%s\n" "$tb"
47+
[[ -n "$zb" ]] && printf "source.zip\t%s\n" "$zb"
48+
fi
49+
}
50+
51+
# Download URL to path with error handling
52+
download_to() {
53+
local url="$1" out_path="$2"
54+
# Remove any existing file first to avoid partial overwrite issues
55+
if [[ -e "$out_path" ]]; then
56+
"$RM" -f "$out_path" || die "Failed to remove existing output: $out_path"
57+
fi
58+
if ! "$CURL" -fL --progress-bar -o "$out_path" "$url"; then
59+
# Clean up any partial file left by curl
60+
[[ -e "$out_path" ]] && "$RM" -f "$out_path" || true
61+
die "Download failed."
62+
fi
63+
}
64+
65+
# Install a .pkg with a custom label and cleanup
66+
install_pkg_with_label() {
67+
local pkg_path="$1" label="${2:-package}"
68+
69+
print_q "Installing ${label} (requires sudo)..."
70+
$SUDO "$INSTALLER" -pkg "$pkg_path" -target / || die "Installer failed."
71+
print_q "Install complete.\n"
72+
if [[ -f "$pkg_path" ]]; then
73+
$RM -f "$pkg_path"
74+
print_q "Removed installer: %s\n" "$pkg_path"
75+
fi
76+
}
77+
78+
79+
# --- version helpers ---
80+
# Extract a core version from any string.
81+
# Prefers X.Y.Z; if not present, returns first X.Y as-is (no normalization).
82+
# Examples:
83+
# - v2.44.0.vfs.0.0 -> 2.44.0
84+
# - git version 2.39.3 (Apple Git-146) -> 2.39.3
85+
# - git version 2.50 (Apple ...) -> 2.50
86+
core_ver() {
87+
local s="$1" v=""
88+
# Prefer first occurrence of X.Y.Z
89+
v=$(printf "%s" "$s" | "$AWK" 'match($0, /[0-9]+(\.[0-9]+){2}/){print substr($0,RSTART,RLENGTH); exit}')
90+
if [[ -z "$v" ]]; then
91+
# Fallback to X.Y and return as-is
92+
v=$(printf "%s" "$s" | "$AWK" 'match($0, /[0-9]+(\.[0-9]+){1}/){print substr($0,RSTART,RLENGTH); exit}')
93+
fi
94+
[[ -n "$v" ]] || v="0.0.0"
95+
printf "%s" "$v"
96+
}
97+
98+
# Compare two versions A and B (both 'X.Y.Z')
99+
# echo -1 if A<B, 0 if A==B, 1 if A>B
100+
vercmp() {
101+
local a b IFS=.
102+
local -a A B
103+
A=(${(s:.:)1})
104+
B=(${(s:.:)2})
105+
while (( ${#A[@]} < 3 )); do A+=(0); done
106+
while (( ${#B[@]} < 3 )); do B+=(0); done
107+
for i in 1 2 3; do
108+
if (( ${A[$i]:-0} < ${B[$i]:-0} )); then echo -1; return; fi
109+
if (( ${A[$i]:-0} > ${B[$i]:-0} )); then echo 1; return; fi
110+
done
111+
echo 0
112+
}
113+
114+
print_ver_status() {
115+
# $1 = tool name, $2 = installed version (or empty), $3 = latest tag
116+
local tool="$1" inst="$2" tag="$3"
117+
local latest core_inst core_latest cmp
118+
latest=$(core_ver "$tag")
119+
if [[ -n "$inst" ]]; then
120+
core_inst=$(core_ver "$inst")
121+
cmp=$(vercmp "$core_inst" "$latest")
122+
case "$cmp" in
123+
-1) printf "$tool: installed is $core_inst, latest is $latest -> update available\n" ;;
124+
0) printf "$tool: installed is $core_inst, latest is $latest -> up-to-date\n" ;;
125+
1) printf "$tool: installed is $core_inst, latest is $latest -> newer than release\n" ;;
126+
esac
127+
else
128+
printf "$tool: not installed, latest $latest available\n"
129+
fi
130+
}
131+
132+
usage() {
133+
cat <<END_OF_USAGE
134+
Usage: $1 [--version <tag>|latest] [--output <output_path|dir>] [--list] [--no-install] [--quiet]
135+
Examples:
136+
$1 Download and install the latest package (prompts for sudo)
137+
$1 --version v2.50.1.vfs.0.2 Download and install a specific package
138+
$1 --list Show the information for the specified (or latest) package but do not download or install
139+
$1 --no-install Show the latest package but do not install
140+
Options:
141+
--version <tag>|latest Release tag or 'latest' for microsoft/git (default: latest)
142+
--output <path|dir> Output file or directory for downloads (default: ~/Downloads/<asset>)
143+
--list List matching assets and version status only (no downloads or installs)
144+
--no-install Download only; do not install. Prints installed vs release versions when applicable
145+
--quiet Reduce output verbosity
146+
-h, --help Show this help and exit
147+
Notes:
148+
- Default download location when --output is omitted: ~/Downloads
149+
- By default this downloads and installs Git (use --no-install to download only).
150+
END_OF_USAGE
151+
}
152+
153+
# --- arg parsing ---
154+
while [[ $# -gt 0 ]]; do
155+
case "$1" in
156+
--output)
157+
OUT=${2:?}; shift 2 ;;
158+
--list)
159+
LIST_ONLY=true; shift ;;
160+
--no-install)
161+
DO_INSTALL=false; shift ;;
162+
--version)
163+
VERSION=${2:-latest}; shift 2 ;;
164+
--quiet)
165+
QUIET=true; shift ;;
166+
-h|--help)
167+
usage $(basename "$0"); exit 0 ;;
168+
*)
169+
err "Unknown argument: $1"; usage; exit 2 ;;
170+
esac
171+
done
172+
173+
local api_url_base="https://api.github.com/repos/${REPO}/releases"
174+
local api_url
175+
if [[ "${VERSION}" == "latest" ]]; then
176+
api_url="${api_url_base}/latest"
177+
else
178+
api_url="${api_url_base}/tags/${VERSION}"
179+
fi
180+
181+
local headers=(
182+
-H 'Accept: application/vnd.github+json'
183+
-H 'X-GitHub-Api-Version: 2022-11-28'
184+
)
185+
186+
print_q "Fetching release metadata from ${api_url} ..."
187+
if ! json=$("$CURL" -fsSL "${headers[@]}" "$api_url"); then
188+
die "Failed to fetch release metadata."
189+
fi
190+
191+
# Determine latest Git tag from JSON
192+
local latest_git_tag=$(printf "%s" "$json" | "$JQ" -r '.tag_name // empty')
193+
194+
# Detect installed version
195+
local installed_ver=""
196+
if command -v git >/dev/null 2>&1; then
197+
installed_ver=$(git --version 2>/dev/null || true)
198+
fi
199+
200+
# Report version status when listing or not installing
201+
if [[ "$LIST_ONLY" == true || "$DO_INSTALL" == false ]]; then
202+
if [[ -n "$latest_git_tag" ]]; then
203+
print_ver_status "Git" "$installed_ver" "$latest_git_tag"
204+
fi
205+
fi
206+
207+
# get the list of available versions and their URLs from JSON
208+
assets=$(extract_assets_from "$json" with-sources)
209+
[[ -n "$assets" ]] || die "No assets found for release '${VERSION}'."
210+
211+
# Keep only assets that end in ".pkg"; others are not Mac assets
212+
local ASSET_REGEX='(\.pkg$)'
213+
matches=$(printf "%s\n" "$assets" | "$GREP" -E -i -- "$ASSET_REGEX" || true)
214+
if [[ -z "$matches" ]]; then
215+
print_q "No assets matched regex: $ASSET_REGEX"
216+
print_q "Available assets (including source tar/zip if provided by release):"
217+
printf "%s\n" "$assets" | "$AWK" -F '\t' '{printf " - %s\n", $1}'
218+
exit 3
219+
fi
220+
221+
# Report what we found if asked
222+
if $LIST_ONLY; then
223+
print_q "Assets for ${REPO}@${VERSION}:"
224+
printf "%s\n" "$matches" | "$AWK" -F '\t' '{printf " - %s\n %s\n", $1, $2}'
225+
exit 0
226+
fi
227+
228+
# Fix up file extensions to be all lowercase
229+
local sel_line=$(printf "%s\n" "$matches" | "$AWK" -F '\t' '$1 ~ /\.[Pp][Kk][Gg]$/ {print; exit}')
230+
if [[ -z "$sel_line" ]]; then
231+
sel_line=$(printf "%s\n" "$matches" | "$HEAD" -n1)
232+
fi
233+
local sel_name=$(printf "%s\n" "$sel_line" | "$AWK" -F '\t' '{print $1}')
234+
local sel_url=$(printf "%s\n" "$sel_line" | "$AWK" -F '\t' '{print $2}')
235+
236+
print_q "Selected asset: $sel_name"
237+
print_q "Download URL: $sel_url"
238+
239+
# Resolve output path
240+
local out_dir
241+
local out_path
242+
if [[ -z "$OUT" ]]; then
243+
out_dir="${HOME}/Downloads"
244+
"$MKDIR" -p "$out_dir"
245+
out_path="${out_dir%/}/${sel_name}"
246+
else
247+
if [[ -d "$OUT" ]]; then
248+
out_path="${OUT%/}/${sel_name}"
249+
else
250+
if [[ "$OUT" == */ ]]; then
251+
"$MKDIR" -p "$OUT"
252+
out_path="${OUT%/}/${sel_name}"
253+
else
254+
out_dir=${OUT:h}
255+
[[ -z "$out_dir" || -d "$out_dir" ]] || "$MKDIR" -p "$out_dir"
256+
out_path="$OUT"
257+
fi
258+
fi
259+
fi
260+
261+
# Download the package unless we're showing information only
262+
print_q "Downloading to: $out_path"
263+
download_to "$sel_url" "$out_path"
264+
print_q "Download complete."
265+
266+
# Do the install if asked
267+
if $DO_INSTALL; then
268+
install_pkg_with_label "$out_path" "Git"
269+
fi

0 commit comments

Comments
 (0)