From fce61919b73e360db5f4c3668c084679551d7c85 Mon Sep 17 00:00:00 2001 From: Erik Schwiebert Date: Wed, 17 Sep 2025 12:06:43 -0700 Subject: [PATCH] Create a shell script for fetching and installing the 'latest' or any specific release tag for ms-git. --- contrib/install-git-mac.sh | 269 +++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100755 contrib/install-git-mac.sh diff --git a/contrib/install-git-mac.sh b/contrib/install-git-mac.sh new file mode 100755 index 00000000000000..a111d1ebfcfd3f --- /dev/null +++ b/contrib/install-git-mac.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env zsh + +# install-git (macOS): Download a Git installer from https://github.com/microsoft/git/releases +# +# macOS-only: This script targets mac (.pkg) assets only, and does not support other platforms. + +set -e +set -u +set -o pipefail + +# --- defaults --- +DO_INSTALL=true # install by default +LIST_ONLY=false +OUT="" # empty -> ~/Downloads/ +QUIET=false +REPO="microsoft/git" +VERSION="latest" + +# --- tool paths (always use absolute paths) --- +AWK="/usr/bin/awk" +CURL="/usr/bin/curl" +GREP="/usr/bin/grep" +HEAD="/usr/bin/head" +INSTALLER="/usr/sbin/installer" +JQ="/usr/bin/jq" +MKDIR="/bin/mkdir" +RM="/bin/rm" +RMDIR="/bin/rmdir" +SUDO="/usr/bin/sudo" +UNAME="/usr/bin/uname" + +err() { printf "[error] %s\n" "$*" 1>&2; } +die() { err "$*"; exit 1; } +print_q() { $QUIET || printf "%s\n" "$*"; } + +# Extract asset name and URL pairs as tab-separated values from provided JSON. +# Arg1: JSON string +# Arg2: with-sources | no-sources (include tarball/zip pseudo-assets) +extract_assets_from() { + local _json="$1" _with_src="${2:-with-sources}" + printf "%s" "$_json" | "$JQ" -r '.assets[] | [.name, .browser_download_url] | @tsv' + if [[ "$_with_src" == "with-sources" ]]; then + local tb zb + tb=$(printf "%s" "$_json" | "$JQ" -r '.tarball_url // empty') + zb=$(printf "%s" "$_json" | "$JQ" -r '.zipball_url // empty') + [[ -n "$tb" ]] && printf "source.tar.gz\t%s\n" "$tb" + [[ -n "$zb" ]] && printf "source.zip\t%s\n" "$zb" + fi +} + +# Download URL to path with error handling +download_to() { + local url="$1" out_path="$2" + # Remove any existing file first to avoid partial overwrite issues + if [[ -e "$out_path" ]]; then + "$RM" -f "$out_path" || die "Failed to remove existing output: $out_path" + fi + if ! "$CURL" -fL --progress-bar -o "$out_path" "$url"; then + # Clean up any partial file left by curl + [[ -e "$out_path" ]] && "$RM" -f "$out_path" || true + die "Download failed." + fi +} + +# Install a .pkg with a custom label and cleanup +install_pkg_with_label() { + local pkg_path="$1" label="${2:-package}" + + print_q "Installing ${label} (requires sudo)..." + $SUDO "$INSTALLER" -pkg "$pkg_path" -target / || die "Installer failed." + print_q "Install complete.\n" + if [[ -f "$pkg_path" ]]; then + $RM -f "$pkg_path" + print_q "Removed installer: %s\n" "$pkg_path" + fi +} + + +# --- version helpers --- +# Extract a core version from any string. +# Prefers X.Y.Z; if not present, returns first X.Y as-is (no normalization). +# Examples: +# - v2.44.0.vfs.0.0 -> 2.44.0 +# - git version 2.39.3 (Apple Git-146) -> 2.39.3 +# - git version 2.50 (Apple ...) -> 2.50 +core_ver() { + local s="$1" v="" + # Prefer first occurrence of X.Y.Z + v=$(printf "%s" "$s" | "$AWK" 'match($0, /[0-9]+(\.[0-9]+){2}/){print substr($0,RSTART,RLENGTH); exit}') + if [[ -z "$v" ]]; then + # Fallback to X.Y and return as-is + v=$(printf "%s" "$s" | "$AWK" 'match($0, /[0-9]+(\.[0-9]+){1}/){print substr($0,RSTART,RLENGTH); exit}') + fi + [[ -n "$v" ]] || v="0.0.0" + printf "%s" "$v" +} + +# Compare two versions A and B (both 'X.Y.Z') +# echo -1 if AB +vercmp() { + local a b IFS=. + local -a A B + A=(${(s:.:)1}) + B=(${(s:.:)2}) + while (( ${#A[@]} < 3 )); do A+=(0); done + while (( ${#B[@]} < 3 )); do B+=(0); done + for i in 1 2 3; do + if (( ${A[$i]:-0} < ${B[$i]:-0} )); then echo -1; return; fi + if (( ${A[$i]:-0} > ${B[$i]:-0} )); then echo 1; return; fi + done + echo 0 +} + +print_ver_status() { + # $1 = tool name, $2 = installed version (or empty), $3 = latest tag + local tool="$1" inst="$2" tag="$3" + local latest core_inst core_latest cmp + latest=$(core_ver "$tag") + if [[ -n "$inst" ]]; then + core_inst=$(core_ver "$inst") + cmp=$(vercmp "$core_inst" "$latest") + case "$cmp" in + -1) printf "$tool: installed is $core_inst, latest is $latest -> update available\n" ;; + 0) printf "$tool: installed is $core_inst, latest is $latest -> up-to-date\n" ;; + 1) printf "$tool: installed is $core_inst, latest is $latest -> newer than release\n" ;; + esac + else + printf "$tool: not installed, latest $latest available\n" + fi +} + +usage() { + cat <|latest] [--output ] [--list] [--no-install] [--quiet] +Examples: + $1 Download and install the latest package (prompts for sudo) + $1 --version v2.50.1.vfs.0.2 Download and install a specific package + $1 --list Show the information for the specified (or latest) package but do not download or install + $1 --no-install Show the latest package but do not install +Options: + --version |latest Release tag or 'latest' for microsoft/git (default: latest) + --output Output file or directory for downloads (default: ~/Downloads/) + --list List matching assets and version status only (no downloads or installs) + --no-install Download only; do not install. Prints installed vs release versions when applicable + --quiet Reduce output verbosity + -h, --help Show this help and exit +Notes: + - Default download location when --output is omitted: ~/Downloads + - By default this downloads and installs Git (use --no-install to download only). +END_OF_USAGE +} + +# --- arg parsing --- +while [[ $# -gt 0 ]]; do + case "$1" in + --output) + OUT=${2:?}; shift 2 ;; + --list) + LIST_ONLY=true; shift ;; + --no-install) + DO_INSTALL=false; shift ;; + --version) + VERSION=${2:-latest}; shift 2 ;; + --quiet) + QUIET=true; shift ;; + -h|--help) + usage $(basename "$0"); exit 0 ;; + *) + err "Unknown argument: $1"; usage; exit 2 ;; + esac +done + +local api_url_base="https://api.github.com/repos/${REPO}/releases" +local api_url +if [[ "${VERSION}" == "latest" ]]; then + api_url="${api_url_base}/latest" +else + api_url="${api_url_base}/tags/${VERSION}" +fi + +local headers=( + -H 'Accept: application/vnd.github+json' + -H 'X-GitHub-Api-Version: 2022-11-28' +) + +print_q "Fetching release metadata from ${api_url} ..." +if ! json=$("$CURL" -fsSL "${headers[@]}" "$api_url"); then + die "Failed to fetch release metadata." +fi + +# Determine latest Git tag from JSON +local latest_git_tag=$(printf "%s" "$json" | "$JQ" -r '.tag_name // empty') + +# Detect installed version +local installed_ver="" +if command -v git >/dev/null 2>&1; then + installed_ver=$(git --version 2>/dev/null || true) +fi + +# Report version status when listing or not installing +if [[ "$LIST_ONLY" == true || "$DO_INSTALL" == false ]]; then + if [[ -n "$latest_git_tag" ]]; then + print_ver_status "Git" "$installed_ver" "$latest_git_tag" + fi +fi + +# get the list of available versions and their URLs from JSON +assets=$(extract_assets_from "$json" with-sources) +[[ -n "$assets" ]] || die "No assets found for release '${VERSION}'." + +# Keep only assets that end in ".pkg"; others are not Mac assets +local ASSET_REGEX='(\.pkg$)' +matches=$(printf "%s\n" "$assets" | "$GREP" -E -i -- "$ASSET_REGEX" || true) +if [[ -z "$matches" ]]; then + print_q "No assets matched regex: $ASSET_REGEX" + print_q "Available assets (including source tar/zip if provided by release):" + printf "%s\n" "$assets" | "$AWK" -F '\t' '{printf " - %s\n", $1}' + exit 3 +fi + +# Report what we found if asked +if $LIST_ONLY; then + print_q "Assets for ${REPO}@${VERSION}:" + printf "%s\n" "$matches" | "$AWK" -F '\t' '{printf " - %s\n %s\n", $1, $2}' + exit 0 +fi + +# Fix up file extensions to be all lowercase +local sel_line=$(printf "%s\n" "$matches" | "$AWK" -F '\t' '$1 ~ /\.[Pp][Kk][Gg]$/ {print; exit}') +if [[ -z "$sel_line" ]]; then + sel_line=$(printf "%s\n" "$matches" | "$HEAD" -n1) +fi +local sel_name=$(printf "%s\n" "$sel_line" | "$AWK" -F '\t' '{print $1}') +local sel_url=$(printf "%s\n" "$sel_line" | "$AWK" -F '\t' '{print $2}') + +print_q "Selected asset: $sel_name" +print_q "Download URL: $sel_url" + +# Resolve output path +local out_dir +local out_path +if [[ -z "$OUT" ]]; then + out_dir="${HOME}/Downloads" + "$MKDIR" -p "$out_dir" + out_path="${out_dir%/}/${sel_name}" +else + if [[ -d "$OUT" ]]; then + out_path="${OUT%/}/${sel_name}" + else + if [[ "$OUT" == */ ]]; then + "$MKDIR" -p "$OUT" + out_path="${OUT%/}/${sel_name}" + else + out_dir=${OUT:h} + [[ -z "$out_dir" || -d "$out_dir" ]] || "$MKDIR" -p "$out_dir" + out_path="$OUT" + fi + fi +fi + +# Download the package unless we're showing information only +print_q "Downloading to: $out_path" +download_to "$sel_url" "$out_path" +print_q "Download complete." + +# Do the install if asked +if $DO_INSTALL; then + install_pkg_with_label "$out_path" "Git" +fi