# Copyright 1999-2013 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Header: $

# @ECLASS: git-r3.eclass
# @MAINTAINER:
# Michał Górny <mgorny@gentoo.org>
# @BLURB: Eclass for fetching and unpacking git repositories.
# @DESCRIPTION:
# Third generation eclass for easing maitenance of live ebuilds using
# git as remote repository. Eclass supports lightweight (shallow)
# clones, local object deduplication and submodules.

case "${EAPI:-0}" in
	0|1|2|3|4|5)
		;;
	*)
		die "Unsupported EAPI=${EAPI} (unknown) for ${ECLASS}"
		;;
esac

if [[ ! ${_GIT_R3} ]]; then

inherit eutils

fi

EXPORT_FUNCTIONS src_unpack

if [[ ! ${_GIT_R3} ]]; then

# @ECLASS-VARIABLE: EGIT_STORE_DIR
# @DESCRIPTION:
# Storage directory for git sources.
#
# EGIT_STORE_DIR=${DISTDIR}/git3-src

# @ECLASS-VARIABLE: EGIT_REPO_URI
# @REQUIRED
# @DESCRIPTION:
# URIs to the repository, e.g. git://foo, https://foo. If multiple URIs
# are provided, the eclass will consider them as fallback URIs to try
# if the first URI does not work.
#
# It can be overriden via env using ${PN}_LIVE_REPO variable.
#
# Example:
# @CODE
# EGIT_REPO_URI="git://a/b.git https://c/d.git"
# @CODE

# @ECLASS-VARIABLE: EVCS_OFFLINE
# @DEFAULT_UNSET
# @DESCRIPTION:
# If non-empty, this variable prevents any online operations.

# @ECLASS-VARIABLE: EGIT_BRANCH
# @DEFAULT_UNSET
# @DESCRIPTION:
# The branch name to check out. If unset, the upstream default (HEAD)
# will be used.
#
# It can be overriden via env using ${PN}_LIVE_BRANCH variable.

# @ECLASS-VARIABLE: EGIT_COMMIT
# @DEFAULT_UNSET
# @DESCRIPTION:
# The tag name or commit identifier to check out. If unset, newest
# commit from the branch will be used. If set, EGIT_BRANCH will
# be ignored.
#
# It can be overriden via env using ${PN}_LIVE_COMMIT variable.

# @ECLASS-VARIABLE: EGIT_NONSHALLOW
# @DEFAULT_UNSET
# @DESCRIPTION:
# Disable performing shallow fetches/clones. Shallow clones have
# a fair number of limitations. Therefore, if you'd like the eclass to
# perform complete clones instead, set this to a non-null value.
#
# This variable is to be set in make.conf. Ebuilds are not allowed
# to set it.

# @FUNCTION: _git-r3_env_setup
# @INTERNAL
# @DESCRIPTION:
# Set the eclass variables as necessary for operation. This can involve
# setting EGIT_* to defaults or ${PN}_LIVE_* variables.
_git-r3_env_setup() {
	debug-print-function ${FUNCNAME} "$@"

	local esc_pn livevar
	esc_pn=${PN//[-+]/_}

	livevar=${esc_pn}_LIVE_REPO
	EGIT_REPO_URI=${!livevar:-${EGIT_REPO_URI}}
	[[ ${EGIT_REPO_URI} ]] \
		|| die "EGIT_REPO_URI must be set to a non-empty value"
	[[ ${!livevar} ]] \
		&& ewarn "Using ${livevar}, no support will be provided"

	livevar=${esc_pn}_LIVE_BRANCH
	EGIT_BRANCH=${!livevar:-${EGIT_BRANCH}}
	[[ ${!livevar} ]] \
		&& ewarn "Using ${livevar}, no support will be provided"

	livevar=${esc_pn}_LIVE_COMMIT
	EGIT_COMMIT=${!livevar:-${EGIT_COMMIT}}
	[[ ${!livevar} ]] \
		&& ewarn "Using ${livevar}, no support will be provided"

	# git-2 unsupported cruft
	local v
	for v in EGIT_{SOURCEDIR,MASTER,HAS_SUBMODULES,PROJECT} \
			EGIT_{NOUNPACK,BOOTSTRAP}
	do
		[[ ${!v} ]] && die "${v} is not supported."
	done
}

# @FUNCTION: _git-r3_set_gitdir
# @USAGE: <repo-uri>
# @INTERNAL
# @DESCRIPTION:
# Obtain the local repository path and set it as GIT_DIR. Creates
# a new repository if necessary.
#
# <repo-uri> may be used to compose the path. It should therefore be
# a canonical URI to the repository.
_git-r3_set_gitdir() {
	debug-print-function ${FUNCNAME} "$@"

	local repo_name=${1#*://*/}

	# strip common prefixes to make paths more likely to match
	# e.g. git://X/Y.git vs https://X/git/Y.git
	# (but just one of the prefixes)
	case "${repo_name}" in
		# cgit can proxy requests to git
		cgit/*) repo_name=${repo_name#cgit/};;
		# pretty common
		git/*) repo_name=${repo_name#git/};;
		# gentoo.org
		gitroot/*) repo_name=${repo_name#gitroot/};;
		# google code, sourceforge
		p/*) repo_name=${repo_name#p/};;
		# kernel.org
		pub/scm/*) repo_name=${repo_name#pub/scm/};;
	esac
	# ensure a .git suffix, same reason
	repo_name=${repo_name%.git}.git
	# now replace all the slashes
	repo_name=${repo_name//\//_}

	local distdir=${PORTAGE_ACTUAL_DISTDIR:-${DISTDIR}}
	: ${EGIT_STORE_DIR:=${distdir}/git3-src}

	GIT_DIR=${EGIT_STORE_DIR}/${repo_name}

	if [[ ! -d ${EGIT_STORE_DIR} ]]; then
		(
			addwrite /
			mkdir -m0755 -p "${EGIT_STORE_DIR}"
		) || die "Unable to create ${EGIT_STORE_DIR}"
	fi

	addwrite "${EGIT_STORE_DIR}"
	if [[ ! -d ${GIT_DIR} ]]; then
		mkdir "${GIT_DIR}" || die
		git init --bare || die

		# avoid auto-unshallow :)
		touch "${GIT_DIR}"/shallow || die
	fi
}

# @FUNCTION: _git-r3_set_submodules
# @USAGE: <file-contents>
# @INTERNAL
# @DESCRIPTION:
# Parse .gitmodules contents passed as <file-contents>
# as in "$(cat .gitmodules)"). Composes a 'submodules' array that
# contains in order (name, URL, path) for each submodule.
_git-r3_set_submodules() {
	debug-print-function ${FUNCNAME} "$@"

	local data=${1}

	# ( name url path ... )
	submodules=()

	local l
	while read l; do
		# submodule.<path>.path=<path>
		# submodule.<path>.url=<url>
		[[ ${l} == submodule.*.url=* ]] || continue

		l=${l#submodule.}
		local subname=${l%%.url=*}

		submodules+=(
			"${subname}"
			"$(echo "${data}" | git config -f /dev/fd/0 \
				submodule."${subname}".url)"
			"$(echo "${data}" | git config -f /dev/fd/0 \
				submodule."${subname}".path)"
		)
	done < <(echo "${data}" | git config -f /dev/fd/0 -l)
}

# @FUNCTION: git-r3_fetch
# @USAGE: <repo-uri> <remote-ref> <local-id>
# @DESCRIPTION:
# Fetch new commits to the local clone of repository. <repo-uri> follows
# the syntax of EGIT_REPO_URI and may list multiple (fallback) URIs.
# <remote-ref> specifies the remote ref to fetch (branch, tag
# or commit). <local-id> specifies an identifier that needs to uniquely
# identify the fetch operation in case multiple parallel merges used
# the git repo. <local-id> usually involves using CATEGORY, PN and SLOT.
#
# The fetch operation will only affect the local storage. It will not
# touch the working copy. If the repository contains submodules, they
# will be fetched recursively as well.
git-r3_fetch() {
	debug-print-function ${FUNCNAME} "$@"

	local repos=( ${1} )
	local remote_ref=${2}
	local local_id=${3}
	local local_ref=refs/heads/${local_id}/__main__

	local -x GIT_DIR
	_git-r3_set_gitdir ${repos[0]}

	# try to fetch from the remote
	local r success
	for r in ${repos[@]}; do
		einfo "Fetching ${remote_ref} from ${r} ..."

		local is_branch lookup_ref
		if [[ ${remote_ref} == refs/heads/* || ${remote_ref} == HEAD ]]
		then
			is_branch=1
			lookup_ref=${remote_ref}
		else
			# ls-remote by commit is going to fail anyway,
			# so we may as well pass refs/tags/ABCDEF...
			lookup_ref=refs/tags/${remote_ref}
		fi

		# first, try ls-remote to see if ${remote_ref} is a real ref
		# and not a commit id. if it succeeds, we can pass ${remote_ref}
		# to 'fetch'. otherwise, we will just fetch everything

		# split on whitespace
		local ref=(
			$(git ls-remote "${r}" "${lookup_ref}")
		)

		# now, another important thing. we may only fetch a remote
		# branch directly to a local branch. Otherwise, we need to fetch
		# the commit and re-create the branch on top of it.

		local ref_param=()
		if [[ ! ${ref[0]} ]]; then
			local EGIT_NONSHALLOW=1
		fi

		if [[ ! -f ${GIT_DIR}/shallow ]]; then
			# if it's a complete repo, fetch it as-is
			:
		elif [[ ${EGIT_NONSHALLOW} ]]; then
			# if it's a shallow clone but we need complete,
			# unshallow it
			ref_param+=( --unshallow )
		else
			# otherwise, just fetch as shallow
			ref_param+=( --depth 1 )
		fi

		if [[ ${ref[0]} ]]; then
			if [[ ${is_branch} ]]; then
				ref_param+=( -f "${remote_ref}:${local_id}/__main__" )
			else
				ref_param+=( "${remote_ref}" )
			fi
		fi

		# if ${remote_ref} is branch or tag, ${ref[@]} will contain
		# the respective commit id. otherwise, it will be an empty
		# array, so the following won't evaluate to a parameter.
		set -- git fetch --no-tags "${r}" "${ref_param[@]}"
		echo "${@}" >&2
		if "${@}"; then
			if [[ ! ${is_branch} ]]; then
				set -- git branch -f "${local_id}/__main__" \
					"${ref[0]:-${remote_ref}}"
				echo "${@}" >&2
				if ! "${@}"; then
					die "Creating branch for ${remote_ref} failed (wrong ref?)."
				fi
			fi

			success=1
			break
		fi
	done
	[[ ${success} ]] || die "Unable to fetch from any of EGIT_REPO_URI"

	# recursively fetch submodules
	if git cat-file -e "${local_ref}":.gitmodules &>/dev/null; then
		local submodules
		_git-r3_set_submodules \
			"$(git cat-file -p "${local_ref}":.gitmodules || die)"

		while [[ ${submodules[@]} ]]; do
			local subname=${submodules[0]}
			local url=${submodules[1]}
			local path=${submodules[2]}
			local commit=$(git rev-parse "${local_ref}:${path}")

			if [[ ! ${commit} ]]; then
				die "Unable to get commit id for submodule ${subname}"
			fi

			git-r3_fetch "${url}" "${commit}" "${local_id}/${subname}"

			submodules=( "${submodules[@]:3}" ) # shift
		done
	fi
}

# @FUNCTION: git-r3_checkout
# @USAGE: <repo-uri> <local-id> <path>
# @DESCRIPTION:
# Check the previously fetched commit out to <path> (usually
# ${WORKDIR}/${P}). <repo-uri> follows the syntax of EGIT_REPO_URI
# and will be used to re-construct the local storage path. <local-id>
# is the unique identifier used for the fetch operation and will
# be used to obtain the proper commit.
#
# If the repository contains submodules, they will be checked out
# recursively as well.
git-r3_checkout() {
	debug-print-function ${FUNCNAME} "$@"

	local repos=( ${1} )
	local local_id=${2}
	local out_dir=${3}

	local -x GIT_DIR GIT_WORK_TREE
	_git-r3_set_gitdir ${repos[0]}
	GIT_WORK_TREE=${out_dir}

	einfo "Checking out ${repos[0]} to ${out_dir} ..."

	mkdir -p "${GIT_WORK_TREE}"
	set -- git checkout -f "${local_id}"/__main__ .
	echo "${@}" >&2
	"${@}" || die "git checkout ${local_id}/__main__ failed"

	# diff against previous revision (if any)
	local new_commit_id=$(git rev-parse --verify "${local_id}"/__main__)
	local old_commit_id=$(
		git rev-parse --verify "${local_id}"/__old__ 2>/dev/null
	)

	if [[ ! ${old_commit_id} ]]; then
		echo "GIT NEW branch -->"
		echo "   repository:               ${repos[0]}"
		echo "   at the commit:            ${new_commit_id}"
	else
		echo "GIT update -->"
		echo "   repository:               ${repos[0]}"
		# write out message based on the revisions
		if [[ "${old_commit_id}" != "${new_commit_id}" ]]; then
			echo "   updating from commit:     ${old_commit_id}"
			echo "   to commit:                ${new_commit_id}"

			git --no-pager diff --color --stat \
				${old_commit_id}..${new_commit_id}
		else
			echo "   at the commit:            ${new_commit_id}"
		fi
	fi
	git branch -f "${local_id}"/{__old__,__main__} || die

	# recursively checkout submodules
	if [[ -f ${GIT_WORK_TREE}/.gitmodules ]]; then
		local submodules
		_git-r3_set_submodules \
			"$(cat "${GIT_WORK_TREE}"/.gitmodules)"

		while [[ ${submodules[@]} ]]; do
			local subname=${submodules[0]}
			local url=${submodules[1]}
			local path=${submodules[2]}

			git-r3_checkout "${url}" "${local_id}/${subname}" \
				"${GIT_WORK_TREE}/${path}"

			submodules=( "${submodules[@]:3}" ) # shift
		done
	fi

	# keep this *after* submodules
	export EGIT_DIR=${GIT_DIR}
	export EGIT_VERSION=${new_commit_id}
}

git-r3_src_fetch() {
	debug-print-function ${FUNCNAME} "$@"

	[[ ${EVCS_OFFLINE} ]] && return

	_git-r3_env_setup
	local branch=${EGIT_BRANCH:+refs/heads/${EGIT_BRANCH}}
	git-r3_fetch "${EGIT_REPO_URI}" \
		"${EGIT_COMMIT:-${branch:-HEAD}}" \
		${CATEGORY}/${PN}/${SLOT}
}

git-r3_src_unpack() {
	debug-print-function ${FUNCNAME} "$@"

	_git-r3_env_setup
	git-r3_src_fetch
	git-r3_checkout "${EGIT_REPO_URI}" \
		${CATEGORY}/${PN}/${SLOT} \
		"${WORKDIR}/${P}"
}

_GIT_R3=1
fi