#!/usr/bin/env bash
#
# Fetch changes from a remote branch to the local GIT repository.
# Copyright (c) Petr Baudis, 2005.
#
# Takes the branch name as an argument, defaulting to 'origin'.
#
# This will fetch the latest changes from a remote repository to the
# corresponding branch in your local repository. Note that this operation
# does not involve merging those changes to your own branch - that is being
# done by the `cg-merge` command. `cg-update` exists to conveniently bundle
# the act of fetching and merging to your working branch together.
#
# Before the first fetch, you have to tell Cogito about the remote branch.
# This should be done by the `cg-branch-add` command. See its documentation
# for the list of supported fetching protocols and other details. Note that
# one exception to this is the 'origin' branch, which was set to the location
# of the source repository if you created yours using the `cg-clone` command.
#
# Note that the operation now being performed by `cg-fetch` ('fetching')
# was called 'pulling' in the past. However, GIT recently changed this
# terminology, and after sufficient transition period, the 'pulling'
# expression will be instead used for the operation now performed by the
# `cg-update` command. Please do not let this confuse you.
#
# OPTIONS
# -------
# -f::
#	Force the complete fetch even if the heads are the same.
#
# -v::
#	Display more verbose output.
#
# ENVIRONMENT
# -----------
# RSYNC::
#	The command to invoke when we want to call the rsync tool (only used
#	when fetching over the rsync protocol). Defaults to 'rsync'.
#
# RSYNC_FLAGS::
#	Additional flags to be passed to the rsync tool when fetching over
#	the rsync protocol.

USAGE="cg-fetch [-f] [-v] [BRANCH_NAME]"

. ${COGITO_LIB}cg-Xlib || exit 1
deprecated_alias cg-fetch cg-pull


fetch_progress()
{
	if [ -t 1 ]; then
		exec "${COGITO_LIB}cg-Xfetchprogress" "$_git_objects"
	else
		exec cat >/dev/null
	fi
}

show_changes_summary()
{
	local orig_head="$1"
	local new_head="$2"
	if [ ! "$orig_head" ]; then
		echo "New branch: $new_head"

	elif [ "$orig_head" != "$new_head" ]; then
		echo "Tree change: $orig_head:$new_head"
		if [ "$verbose" ] ; then
			git-diff-tree -r "$(cg-object-id -t "$orig_head")" "$(cg-object-id -t "$new_head")"
		else
			git-diff-tree -r "$(cg-object-id -t "$orig_head")" "$(cg-object-id -t "$new_head")" | \
				awk '{ print $5" "$6 }'
		fi
	else
		echo "Up to date."
	fi
}


get_rsync()
{
	[ "$1" = "-b" ] && shift

	redir=
	if [ "$1" = "-i" ]; then # ignore-errors
		redir="2>/dev/null"
		shift
	fi

	filter="cat"
	if [ "$1" = "-s" ]; then # subsequent
		# We already saw the MOTD, thank you very much.
		filter="grep -v ^MOTD:"
		shift
	fi

	rsync_flags_l=
	if [ "$1" = "-u" ]; then # update
		rsync_flags_l="--ignore-existing"
		shift
	fi

	appenduri=
	if [ "$1" = "-d" ]; then # directory
		appenduri="/." # CowboyNeal
		shift
	fi

	eval '"${RSYNC:-rsync}"' $RSYNC_FLAGS $rsync_flags_l -v -Lr \
		'"$1$appenduri"' '"$2$appenduri"' $redir | $filter
	return ${PIPESTATUS[0]}
}

fetch_rsync()
{
	get_rsync -s -u -d "$2/objects" "$_git_objects" | fetch_progress
	ret=${PIPESTATUS[0]}
	if [ "$3" ] && [ "$ret" -eq "0" ]; then
		if [ "$orig_head" ]; then
			git-rev-list --objects $new_head ^$orig_head |
				while read obj type; do
					git-cat-file -t $obj >/dev/null || exit $?
				done ||
			die "rsync fetch incomplete, some objects missing"
		fi
		cat "$_git/refs/${3%/*}/.${3##*/}-fetching" > "$_git/refs/$3"
	fi
	return $ret
}


get_http()
{
	[ "$1" = "-b" ] && shift
	[ "$1" = "-i" ] && shift
	[ "$1" = "-s" ] && shift
	[ "$1" = "-u" ] && die "INTERNAL ERROR: HTTP no-clobber not implemented"
	[ "$1" = "-d" ] && die "INTERNAL ERROR: HTTP recursive not implemented"

	src="$1"
	dest="$2"

	curl_extra_args=
	[ "$GIT_SSL_NO_VERIFY" ] && curl_extra_args="-k"
	curl -nsfL $curl_extra_args -o "$dest" "$src"
}

fetch_http()
{
	whead=
	[ "$3" ] && whead="-w $3"
	(git-http-fetch -a -v $whead $recovery "$1" "$2/" 2>&1 /dev/null) | fetch_progress
	return ${PIPESTATUS[0]}
}


get_local()
{
	cp_flags_l="-v"
	if [ "$1" = "-b" ]; then
		# Dereference symlinks
		cp_flags_l="$cp_flags_l -L"
		shift
	else
		cp_flags_l="$cp_flags_l -pRP"
	fi

	[ "$1" = "-i" ] && shift
	[ "$1" = "-s" ] && shift
	[ "$1" = "-u" ] && die "INTERNAL ERROR: local-fetch no-clobber not implemented"
	[ "$1" = "-d" ] && die "INTERNAL ERROR: local-fetch recursive not implemented"

	src="$1"
	dest="$2"

	cp $cp_flags_l "$src" "$dest"
}

fetch_local()
{
	whead=
	[ "$3" ] && whead="-w $3"
	(git-local-fetch -a -l -v $whead $recovery "$1" "$2" 2>&1 /dev/null) | fetch_progress
	return ${PIPESTATUS[0]}
}


fetch_tags()
{
	echo "Fetching tags..."

	# FIXME: Warn about conflicting tag names?
	[ -d "$_git/refs/tags" ] || mkdir -p "$_git/refs/tags"

	if [ "$get" = "get_rsync" ]; then
		if ! $get -i -s -u -d "$uri/refs/tags" "$_git/refs/tags"; then
			echo "unable to get tags list (non-fatal)" >&2
			return $?
		fi
	fi

	git-ls-remote --tags "$uri" |
		# SHA1 refs/tags/v0.99.8^{} --> SHA1 tags/v0.99.8
		# where SHA1 is the object v0.99.8 tag points at.
		sed -ne 's:\([^	]\)	refs/\(tags/.*\)^{}$:\1 \2:p' |
		while read sha1 tagname; do
			# Do we have the tag itself?
			[ -s "$_git/refs/$tagname" ] && continue
			# Do we have the object pointed at by the tag?
			git-cat-file -t "$sha1" >/dev/null 2>&1 || continue

			# if so, fetch the tag -- which should be
			# a cheap operation -- to complete the chain.
			echo -n "Missing tag ${tagname#tags/}... "
			if $fetch "$tagname" "$uri" "$tagname" 2>/dev/null >&2; then
				echo "retrieved"
			else
				# 17 is code from packed transport, which
				# will grab all of them en masse later
				if [ "$?" -ne "17" ]; then
					echo "unable to retrieve"
				else
					echo ""
				fi
			fi
		done
	[ "${PIPESTATUS[0]}" -eq "0" ] ||
		echo "unable to get tags list (non-fatal)" >&2
	return 0
}


recovery=
verbose=
while optparse; do
	if optparse -f; then
		# When forcing, let the fetch tools make more extensive
		# walk over the dependency tree with --recover.
		recovery=--recover
	elif optparse -v; then
		verbose=1
	else
		optfail
	fi
done

name=${ARGS[0]}

[ "$name" ] || { [ -s "$_git/branches/origin" ] && name=origin; }
[ "$name" ] || die "where to fetch from?"
uri=$(cat "$_git/branches/$name" 2>/dev/null) || die "unknown branch: $name"

rembranch=
if echo "$uri" | grep -q '#'; then
	rembranch=$(echo "$uri" | cut -d '#' -f 2)
	uri=$(echo "$uri" | cut -d '#' -f 1)
fi

# Some other process with the same pid might appear, that's why
# we won't die but rather let the user check quickly.
if [ -s "$_git/info/cg-fetch-$name-dirty" ]; then
	kill -0 $(cat "$_git/info/cg-fetch-$name-dirty") 2>/dev/null && \
		echo "Warning: Aren't you fetching $name twice at once? (waiting 10s)" >&2 && \
		sleep 10
	echo "Recovering from a previously interrupted fetch..."
	recovery=--recover
fi
mkdir -p "$_git/info"
echo $$ > "$_git/info/cg-fetch-$name-dirty"


orig_head=
[ -s "$_git/refs/heads/$name" ] && orig_head="$(cat "$_git/refs/heads/$name")"


packed_transport=

if echo "$uri" | grep -q "^http://"; then
	get=get_http
	fetch=fetch_http
elif echo "$uri" | grep -q "^git+ssh://"; then
	packed_transport=ssh
elif echo "$uri" | grep -q "^git://"; then
	packed_transport=git
elif echo "$uri" | grep -q "^rsync://"; then
	echo "WARNING: The rsync access method is DEPRECATED and will be REMOVED in the future!" >&2
	get=get_rsync
	fetch=fetch_rsync
elif echo "$uri" | grep -q ":"; then
	echo "WARNING: I guessed the host:path syntax was used and fell back to the git+ssh protocol." >&2
	echo "WARNING: The host:path syntax is evil because it is implicit. Please just use a URI." >&2
	packed_transport=ssh
else
	[ -d "$uri/.git" ] && uri="$uri/.git"
	[ -d "$uri" ] || die "repository not found"
	get=get_local
	fetch=fetch_local

	# Perhaps the object database is shared
	symlinked=
	is_same_repo "$_git_objects" "$uri/objects" && symlinked=1

	# See if we can hardlink and add "-l" to cp flags.
	can_hardlink=
	sample_file="$(find $uri -type f -print | head -n 1)"
	rm -f "$_git/.,,lntest"
	if cp -fl "$sample_file" "$_git/.,,lntest" 2>/dev/null; then
		can_hardlink=l
		echo "Using hard links"
	else
		echo "Hard links don't work - using copy"
	fi
	rm -f "$_git/.,,lntest"
fi


if [ "$packed_transport" ]; then
	# This is a really special case.
	[ "$rembranch" ] || rembranch="HEAD"

	fetch_pack_recorder () {
		while read sha1 remote_name; do
			[ "$sha1" = "failed" ] && die "$2"
			ref="$1"; [ "$ref" ] || ref="$remote_name"
			echo "$sha1" >"$_git/$ref"
		done
	}
	echo "Fetching pack (head and objects)..."
	( git-fetch-pack "$uri" "$rembranch" ||
	  echo "failed" "$rembranch" ) |
		fetch_pack_recorder "refs/heads/$name" "fetching pack failed" ||
		exit

	export _cg_taglist="$(mktemp -t gitfetch.XXXXXX)"
	record_tags_to_fetch () {
		echo "refs/$1" >>"$_cg_taglist"
		return 17
	}
	fetch=record_tags_to_fetch
	fetch_tags
	if [ -s "$_cg_taglist" ]; then
		( cat "$_cg_taglist" | tr '\n' '\0' |
			xargs -0 git-fetch-pack "$uri" ||
		  echo "failed" "$rembranch" ) |
		fetch_pack_recorder "" "unable to retrieve tags (non-fatal)"
	fi
	rm "$_cg_taglist"

	rm "$_git/info/cg-fetch-$name-dirty"
	show_changes_summary "$orig_head" "$(cg-object-id "$name")"
	exit 0
fi


### Behold, the fetch itself

## Grab the head
echo "Fetching head..."
mkdir -p "$_git/refs/heads"
if [ "$rembranch" ]; then
	$get -i "$uri/refs/heads/$rembranch" "$_git/refs/heads/.$name-fetching" ||
		die "unable to get the head pointer of branch $rembranch"
else
	$get -b "$uri/HEAD" "$_git/refs/heads/.$name-fetching" ||
		die "unable to get the HEAD branch"
fi

new_head="$(cat "$_git/refs/heads/.$name-fetching")"
if [ "${new_head#ref:}" != "$new_head" ]; then
	new_head="$(echo "$new_head" | sed 's/^ref: *//')"
	$get -i "$uri/$new_head" "$_git/refs/heads/.$name-fetching" ||
		die "unable to get the head pointer of branch $new_head (referenced by HEAD)"
	new_head="$(cat "$_git/refs/heads/.$name-fetching")"
fi

echo "Fetching objects..."
## Fetch the objects
if ! [ "$symlinked" ]; then
	if [ "$recovery" -o "$orig_head" != "$new_head" ]; then
		[ -d "$_git_objects" ] || mkdir -p "$_git_objects"
		$fetch "$(cat "$_git/refs/heads/.$name-fetching")" "$uri" "heads/$name" || die "objects fetch failed"
	fi
else
	cat "$_git/refs/heads/.$name-fetching" > "$_git/refs/heads/$name"
fi
rm "$_git/refs/heads/.$name-fetching"

## Fetch the tags
ret=0
if ! fetch_tags; then
	ret=$?
fi

rm "$_git/info/cg-fetch-$name-dirty"
show_changes_summary "$orig_head" "$new_head"
exit $ret
