#!/usr/bin/env bash
# pull -- mass-update Git repositories to latest upstream versions

. lib.bash || exit

do_git() {
	export PULL_DIFFSTAT=1
	if (( !opt_tig )); then
		export PULL_NONINTERACTIVE=1
	fi
	git-up
}

do_pullfile() {
	local -A skip_dirs=()
	local skip_args=
	local skip_glob=
	local -i positive=0

	if [[ -s Pullfile ]]; then
		set -- $(grep '^[^#]' Pullfile)
		for arg; do
			case $arg in
			!*|-*)	true ;;
			*)	let ++positive ;;
			esac
		done
		if (( !positive )); then
			debug "No targets in Pullfile, assuming */"
			set -- "$@" */
		fi
	elif [[ -e Pullfile ]]; then
		set -- */
	else
		set -- .
	fi

	for arg; do
		arg=${arg%/}
		case $arg in
		!*)
			arg=${arg#!}
			debug "Excluding glob '$arg'"
			skip_args+="|$arg"
			skip_glob="@(${skip_args#|})"
			;;
		-*)
			arg=${arg#-}
			arg=$(realpath "$arg")
			debug "Excluding path '$arg'"
			skip_dirs[$arg]=1
			;;
		*)
			if [[ $skip_glob && $arg == $skip_glob ]]; then
				debug "Skipping '$arg' (matches exclude)"
				continue
			fi
			arg=$(realpath "$arg")
			if [[ ! -d $arg ]]; then
				warn "Skipping '$arg' (not a directory)"
				continue
			fi
			if [[ ${skip_dirs[$arg]-} ]]; then
				debug "Skipping '$arg' (path excluded)"
				continue
			fi
			(cd "$arg" && main)
			;;
		esac
	done
}

main() {
	local argdepth=${argdepth:-0}
	local rdir=

	# Determine relative path for display purposes
	rdir=$(realpath --relative-to="$_PULL_STARTDIR" "$PWD")
	if [[ $rdir == . ]]; then
		rdir=$PWD
	elif [[ $rdir == ../* ]]; then
		# Fall back to absolute path if there are no common components
		# (to avoid "../../../../net/etc")
		rdirtemp=$rdir
		sdirtemp=${_PULL_STARTDIR%/}
		while [[ $rdirtemp == ../* ]]; do
			rdirtemp=${rdirtemp#../}
			sdirtemp=${sdirtemp%/*}
		done
		if [[ ! $sdirtemp ]]; then
			rdir=$PWD
		fi
	fi

	# Check cwd for supported repositories
	if [[ -f Pullfile ]]; then
		if (( opt_list > 1 && argdepth > 0 )); then
			echo "$rdir"
		elif (( opt_dryrun > 1 && argdepth > 0 )); then
			vmsg "Would descend into '$rdir'" >&2
		else
			(let argdepth+=1; do_pullfile)
		fi
	elif [[ -e .git ]]; then
		if (( opt_list )); then
			echo "$rdir"
		elif (( opt_dryrun )); then
			vmsg "Would update '$rdir'" >&2
		else
			log2 "Updating '$rdir'"
			do_git || err "Update failed for '$rdir'"
		fi
	else
		if (( opt_dryrun )); then
			vmsg "Would fail '$rdir'" >&2
		else
			log2 "Updating '$rdir'"
			err "Not a repository: '$rdir'"
		fi
	fi
}

usage() {
	echo "Usage: $progname [-lnt] [DIR...]"
	echo
	echo_opt "-l"	"only list paths to be updated (implies '-n')"
	echo_opt "-n"	"dry run (parse Pullfile but don't update anything)"
	echo_opt "-nn"	"dry run (do not recurse)"
	echo_opt "-t"	"immediately run 'tig' to browse commits"
}

set -u

# Remember original starting directory for displaying subdirs
_PULL_STARTDIR=$PWD

opt_dryrun=0
opt_list=0
opt_tig=0

while getopts ":lnt" OPT; do
	case $OPT in
	l) let ++opt_list;;
	n) let ++opt_dryrun;;
	t) let ++opt_tig;;
	*) lib:die_getopts;;
	esac
done; shift $((OPTIND-1))

if (( opt_tig )) && ! have tig; then
	vdie "tig is not installed"
fi

if (( !$# )); then
	# When invoked as bare 'pull [-t]', try to DWIM and guess the
	# repository in case we're running in a subdir.
	if [[ ! -e Pullfile ]]; then
		if dir=$(git rev-parse --show-cdup 2> /dev/null); then
			cd "$dir"
		fi
	fi
	set -- .
fi

for arg; do
	if [[ -d $arg ]]; then
		(cd "$arg" && main)
	else
		vdie "path is not a directory: $arg"
	fi
done
