#!/usr/bin/env bash
#
# Fetch changes from a remote branch to the local 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.
#	Force the complete fetch even if the heads are the same.
#
# -v:: Enable verbosity
#	Display more verbose output - most notably list all the files
#	touched by the fetched changes. Use twice to get even more verbosity,
#	that is raw progress information instead of the progress bar.
#
# 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.

# Testsuite: Largely covered (t91xx testsuite family, incomplete coverage)


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

. "${COGITO_LIB}"cg-Xlib || exit 1


fetch_progress()
{
	[ $verbose -ge 2 ] && exec cat
	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"
		[ $verbose -ge 1 ] && git-diff-tree --abbrev -r "$(cg-object-id -t "$orig_head")" "$(cg-object-id -t "$new_head")"
	else
		echo "Up to date."
	fi
}

initial_done()
{
	rm -f ___
	cp "$_git/refs/heads/origin" "$_git/refs/heads/master" ||
		die "initial checkout failed"
	if [ -s "$_git/info/cg-fetch-initial-wcless" ]; then
		rm "$_git/info/cg-fetch-initial-wcless"
	else
		git-read-tree HEAD &&
		git-checkout-index -a &&
		git-update-index --refresh ||
		die "initial checkout failed"
	fi
	rm "$_git/info/cg-fetch-initial"
}


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

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

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

fetch_rsync_verify()
{
	if [ $verbose -ge 2 ]; then
		# We must not pipe to prevent buffered I/O
		get_rsync -s -d "$2/objects" "$_git_objects"
	else
		get_rsync -s -d "$2/objects" "$_git_objects" | fetch_progress
	fi

	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
}


fetch_rsync()
{
	if [ x"$1" = x"--stdin" ]; then
		while read c w; do
			echo "$c" >"$_git/refs/$w"
		done
	else
		fetch_rsync_verify "$1" "$2" "$3"
	fi
}

get_http()
{
	[ "$1" = "-b" ] && shift
	[ "$1" = "-i" ] && shift
	[ "$1" = "-s" ] && shift
	[ "$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"
	cp_flags_l=
	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" = "-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 -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 -n -e 's:\([^	]\)	refs/\(tags/.*\)^{}$:\1 \2:p' \
		       -e '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 "Missing tag ${tagname#tags/}..." >&2
			echo -e "$tagname"\\t"$tagname"
		done |
		sort | uniq | $fetch --stdin "$uri"
	if [ "${PIPESTATUS[0]}" -ne 0 -o "$?" -ne 0 ]; then
		echo "unable to fetch tags (non-fatal)" >&2
	fi
	return 0
}


recovery=
verbose=0
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=$((verbose+1))
	else
		optfail
	fi
done

name="${ARGS[0]}"

[ "$name" ] || name="$(choose_origin branches "where to fetch from?")" || exit 1
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

if [ "$_git_no_wc" ]; then
	[ -s "$_git/info/cg-fetch-initial" ] && [ ! -s "$_git/info/cg-fetch-initial-wcless" ] &&
		die "you must run the initial cg-fetch from the working copy root directory"
fi

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


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


packed_transport=

if echo "$uri" | grep -q "^\(https\?\|ftp\)://"; 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"

	cloneorfetch= #fetch
	[ -s "$_git/info/cg-fetch-initial" ] && cloneorfetch=-k #clone

	rm -f "$_git/info/cg-fetch-earlydie"

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

	record_tags_to_fetch () {
		( cut -f 1 | tr '\n' '\0' |
			xargs -0 git-fetch-pack $cloneorfetch "$uri" ||
		  echo "failed" "$rembranch" ) |

		fetch_pack_recorder "" "unable to retrieve tags (non-fatal)"
	}
	fetch=record_tags_to_fetch
	fetch_tags

	rm "$dirtyfile"
	show_changes_summary "$orig_head" "$(cg-object-id "$name")"
	[ -s "$_git/info/cg-fetch-initial" ] && initial_done
	exit 0
fi


### Behold, the fetch itself

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

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

rm -f "$_git/info/cg-fetch-earlydie"

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/$tmpname")" "$uri" "heads/$name" || die "objects fetch failed"
	fi
else
	cat "$_git/refs/heads/$tmpname" > "$_git/refs/heads/$name"
fi
rm "$_git/refs/heads/$tmpname"

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

rm "$dirtyfile"
show_changes_summary "$orig_head" "$new_head"
[ -s "$_git/info/cg-fetch-initial" ] && initial_done
exit $ret
