#!/bin/sh

# Compares the currently installed Git for Windows against latest available
# release.  If versions differ, the bit matched installer is downloaded and run
# when confirmation to do so is given.


# Compare version strings
# Prints -1, 0 or 1 to stdout

version_compare () {
	a="$1"
	b="$2"

	while true
	do
		test -n "$b" || { echo 1; return; }
		test -n "$a" || { echo -1; return; }

		# Get the first numbers (if any)
		a1="$(expr "$a" : '^\([0-9]*\)')"; a="${a#$a1}"
		b1="$(expr "$b" : '^\([0-9]*\)')"; b="${b#$b1}"

		if test -z "$b1"
		then
			test -z "$a1" || { echo 1; return; }
			a1=0
			b1=0
		fi
		test -n "$a1" || { echo -1; return; }
		test $a1 -le $b1 || { echo 1; return; }
		test $b1 -le $a1 || { echo -1; return; }

		# Skip non-numeric prefixes
		a1="$(expr "$a" : '^\([^0-9]\+\)')"; a="${a#$a1}"
		b1="$(expr "$b" : '^\([^0-9]\+\)')"; b="${b#$b1}"

		case "$a1,$b1" in
		[.-]rc,[.-]rc) ;; # both are -rc versions
		[.-]rc,*) echo -1; return;;
		*,[.-]rc) echo 1; return;;
		esac
	done
}

test "--test-version-compare" != "$*" || {
	test_version_compare () {
		result="$(version_compare "$1" "$2")"
		test "$3" = "$result" || {
			echo "version_compare $1 $2 returned $result instead of $3" >&2
			exit 1
		}

		result2="$(version_compare "$2" "$1")"
		test "$result2" = "$((-$3))" || {
			echo "version_compare $2 $1 returned $result2 instead of $((-$3))" >&2
			exit 1
		}
	}

	test_version_compare 2.32.0.windows.1 2.32.1.windows.1 -1
	test_version_compare 2.32.1.windows.1 2.32.0.windows.1 1
	test_version_compare 2.32.1.vfs.0.0 2.32.0.windows.1 1
	test_version_compare 2.32.1.vfs.0.0 2.32.0.vfs.0.0 1
	test_version_compare 2.32.0.vfs.0.1 2.32.0.vfs.0.2 -1
	test_version_compare 2.32.0-rc0.windows.1 2.31.1.windows.1 1
	test_version_compare 2.32.0-rc2.windows.1 2.32.0.windows.1 -1
	test_version_compare 2.34.0.rc1.windows.1 2.33.1.windows.1 1
	test_version_compare 2.34.0.rc2.windows.1 2.34.0.windows.1 -1
	exit 0
}

# Counts how many Bash instances are running, apart from the current one (if
# any: `git update-git-for-windows` might have been called from a CMD window,
# in which case no Git Bash might be running at all).
#
# This is a little tricky, as the /usr/bin/sh process (as which `ps` reports the
# process running this script) is an MSYS2 one, but the calling `git.exe`
# process is a pure Win32 one. As a consequence, the former process' PPID will
# be reported as 1 (!!!) and its PGID will refer to the latter, while the
# latter's PGID will be identical to its PID and its PPID refers to the calling
# Bash (or is 1, if `git.exe` was not called by an MSYS2 program).
#
# So we have to employ a little sed fu to parse `ps` output of the form:
#
#     PID    PPID    PGID     WINPID   TTY         UID    STIME COMMAND
#   19864   15640   19864      27996  pty0     4853009 15:58:05 /usr/bin/bash
#   15640       1   15640      15640  ?        4853009 15:58:05 /usr/bin/mintty
#   28128   13048   21176      28716  pty0     4853009 16:01:08 /usr/bin/ps
#   13048       1   21176      13048  pty0     4853009 16:01:08 /usr/bin/sh
#   21176   19864   21176      11996  pty0     4853009 16:01:08 /mingw64/bin/git
#
# Essentially, we are looking for the /usr/bin/sh line (in the example, PID
# 13048), follow its PGID to the /mingw64/bin/git line (in the example, PID
# 21176), and record the PPID of the latter as the pid of the current Bash, if
# any. As we do not know in which order the `sh` and the `git` line appear, we
# have to handle both orders.
#
# Then, we filter the `ps` output first by dropping the line with the current
# Bash, then finally counting the remaining lines referring to a bash process.

count_other_bashes () {
	mypid=$$ && nl='\n *' && s='  *' && p='[1-9][0-9]*' &&
	mypid="$(ps | sed -n ":1;N;
		s/.*$nl$mypid$s$p$s\\($p\\) .*$nl\\1$s\\($p\\) .*/\\2/p;
		s/.*$nl\\($p\\)$s\\($p\\) .*$nl$mypid$s$p$s\\1 .*/\\2/p;
		b1")"
	ps |
	if test -z "$mypid"; then cat; else grep -v "^ *$mypid "; fi |
	grep ' /usr/bin/bash$' |
	wc -l
}

# Write HTTP GET response to stdout and return error code. This is equivalent
# to curl --fail, except that we output the response body to stderr in case of
# an HTTP error status.
http_get () {
	url=$1
	output=$(mktemp -t gfw-httpget-XXXXXXXX.txt)
	code=$(curl \
		--silent \
		--show-error \
		--output "$output" \
		--write-out '%{http_code}' \
		"$url") || return $?
	fdout=1
	ret=0
	if test "$code" -ge 400
	then
		fdout=2
		ret=22
	fi
	cat "$output" >&"$fdout"
	rm -f "$output"
	return "$ret"
}

get_recently_seen() {
    if [ -f "$HOME"/.git-for-windows-updater ]; then
        git config -f "$HOME"/.git-for-windows-updater winUpdater.recentlySeenVersion
    else
        git config --global winUpdater.recentlySeenVersion
    fi
}

set_recently_seen() {
    git config -f "$HOME"/.git-for-windows-updater winUpdater.recentlySeenVersion "$1"
}

# The main function of this script

update_git_for_windows () {
	proxy=$(git config --get http.proxy)
	if test -n "$proxy"
	then
		export https_proxy="$proxy"
		echo "Using proxy server $https_proxy detected from git http.proxy" >&2
	fi

	yn=
	use_gui=
	quiet=
	testing=
	while test $# -gt 0
	do
		case "$1" in
		-\?|--?\?|-h|--help) ;;
		-y|--yes) yn=y; shift; continue;;
		-g|--gui) use_gui=t; shift; continue;;
		--quiet) quiet=t; shift; continue;;
		--testing) testing=t; shift; continue;;
		*) echo "Unknown option: $1" >&2;;
		esac
		printf >&2 '%s\n%s\n\t%s\n\t%s\n' \
			"Usage: git update-git-for-windows [options]" \
			"Options:" \
			"-g, --gui Use GUI instead of terminal to prompt" \
			"-y, --yes Automatic yes to download and install prompt" \
			"Return code:" \
			" 0: no update available" \
			" 1: update available and user selected not to update" \
			" 2: update available and it was started"
		return 1
	done

	case "$(uname -m)" in
	x86_64) bit=64;;
	*) bit=32;;
	esac

	try_toast=
	test -z "$use_gui" ||
	case "$(uname -s)" in
	*-6.[23]|*-6.[23]-[1-9]*|*-10.0|*-10.0-[1-9]*)
		# Only try to show a Toast notification on Windows 8 & 10,
		# and only if we have a working wintoast.exe
		! type wintoast.exe >/dev/null 2>&1 ||
		try_toast=t
		;;
	esac

	test -f "$0.config" &&
	fork="$(git config -f "$0.config" update.fromFork)" &&
	test -n "$fork" ||
	fork=

	if test -n "$fork"
	then
		git_label="$(git config -f "$0.config" update.gitLabel)"
		test -n "$git_label" || git_label=Git

		releases_url=https://api.github.com/repos/$fork/releases
		latest_tag_url=$releases_url/latest
		latest_eval='latest=${latest_tag#*\"tag_name\": \"v}; latest=${latest%%\"*}'
	else
		git_label="Git for Windows"
		releases_url=https://api.github.com/repos/git-for-windows/git/releases
		latest_tag_url=https://gitforwindows.org/latest-tag.txt
		latest_eval='latest=${latest_tag#v}'
	fi

	latest_tag=$(http_get $latest_tag_url) ||
	case $?,"$proxy" in
	7,)
		proxy="$(proxy-lookup.exe https://gitforwindows.org)" &&
		test -n "$proxy" &&
		export https_proxy="$proxy" &&
		echo "Using proxy $https_proxy as per lookup" >&2 &&
		latest_tag=$(http_get $latest_tag_url) ||
		return
		;;
	*)
		return
		;;
	esac

	eval "$latest_eval"
	# Be extra careful to remove a leading 'v' from the tag.
	latest=${latest#v}

	# Did we ask about this version already?
	if test -z "$use_recently_seen"
	then
		recently_seen="$(get_recently_seen)"
		test -n "$quiet" && test "x$recently_seen" = "x$latest" && return
	fi

	version=$(git --version | sed "s/git version //")
	if test -d /arm64/bin
	then
		arch_bit=ARM64
	else
		arch_bit=${bit}-bit
	fi

	echo "$git_label $version ($arch_bit)" >&2
	if test -z "$testing" && test "$latest" = "$version"
	then
		echo "Up to date" >&2
		set_recently_seen "$latest"
		return
	fi
	if ! test -n "$testing"
	then
		# We are not testing and we don't have exact equality,
		# so do a careful comparison and look to see if the
		# latest release is strictly newer than ours.
		if test 0 -lt "$(version_compare "$version" "$latest")"
		then
			return
		fi
	fi

	echo "Update $latest is available" >&2
	releases=$(http_get $releases_url/latest) || return
	download=$(echo "$releases" |
		grep '"browser_download_url": "' |
		grep "$arch_bit\.exe" |
		sed -E 's/.*": "([^"]*).*/\1/')
	filename=$(echo "$download" | sed -E 's/.*\/([^\/]*)$/\1/')
	name="$(echo "$releases" | sed -n 's/^  "name": "\(.*\)",$/\1/p')"
	installer=$(mktemp -t gfw-install-XXXXXXXX.exe)
	if test -z "$yn"
	then
		other_bashes=$(count_other_bashes)
		if test $other_bashes -le 0
		then
			warn=
		elif test $other_bashes -eq 1
		then
			warn=" (killing one Git Bash)"
		else
			warn=" (killing $other_bashes Git Bash instances)"
		fi
		if test -n "$try_toast"
		then
			wintoast.exe --appname "$git_label" \
				--appid GitForWindows.Updater \
				--image /mingw$bit/share/git/git-for-windows.ico \
				--text "Download and install $name$warn?" \
				--action Yes --action No --expirems 15000
			case $? in
			0|16)
				# clicked toast, or clicked Yes: download
				;;
			1|17)
				# dismiseed, or clicked No: ignore this release
				set_recently_seen "$latest"
				return 1
				;;
			4|5|6|9|10)
				# toast not activated, failed, toasts
				# unsupported, WinToast init failed or toast
				# not launched: fall back to using GUI
				git askyesno \
					--title "Git Update Available" \
					"Download and install $name$warn?" || {
				    set_recently_seen "$latest"
				    return 1
				}
				;;
			*)
				# toast timed out, or hidden, or unknown
				# failure: ignore
				return 1
				;;
			esac
		elif test -n "$use_gui"
		then
			git askyesno --title "Git Update Available" \
				"Download and install $name$warn?" || {
				set_recently_seen "$latest"
				return 1
			}
		else
			read -p "Download and install $name$warn [N/y]? " \
				yn >&2
			case "$yn" in
			[Yy]*) ;;
			*)
				set_recently_seen "$latest"
				return 1;;
			esac
		fi
	else
		echo "Downloading $filename" >&2
	fi
	curl -# -L -o $installer $download || return
	start "" "$installer" //SILENT //NORESTART

	# Kill all Bash processes (which will let MinTTY quit, too)"
	#
	# `ps` without `-W` will automatically only catch MSYS2 processes
	# that link to *this* MSYS2 runtime, i.e. no processes from other Git
	# installations (e.g. Git for Windows' SDK) will be killed.
	ps | grep ' /usr/bin/bash$' | awk '{print "kill -9 " $1 ";" }' | sh
	return 2
}

update_git_for_windows "$@"
