#!/usr/bin/env zsh
## caretaker - /home package manager and zsh playground
## Copyright © 2008-2010 by Daniel Friesel <derf@finalrewind.org>
##
## Permission to use, copy, modify, and/or distribute this software for any
## purpose with or without fee is hereby granted, provided that the above
## copyright notice and this permission notice appear in all copies.
##
## THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
## WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
## MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
## ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
## WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
## ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
## OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

setopt extended_glob
typeset -a -U triggers
typeset -a PKG_ROOTS CL_OPTIONS

if [[ -t 1 ]] {
	c_info=$'\e[0;36m'
	c_error=$'\e[0;31m'
	c_reset=$'\e[0m'
}

## Basic output functions

function info {
	echo -ne - "${c_info}${*}${c_reset}"
}

function warn {
	echo -ne - "${c_error}${*}${c_reset}" > /dev/stderr
}

function die {
	echo -ne - "${c_error}${*}${c_reset}" > /dev/stderr
	exit 100
}

function say {
	echo - ${*}
}

function clear_line {
	echo -ne "\r\e[2K"
}

: ${XDG_CONFIG_HOME=${HOME}/.config}
if [[ -r ${XDG_CONFIG_HOME}/caretaker/caretaker.conf ]] {
	source ${XDG_CONFIG_HOME}/caretaker/caretaker.conf
} elif [[ -r ${HOME}/.caretaker.conf ]] {
	source ${HOME}/.caretaker.conf
}

while [[ ${1} == --* ]] {
	case ${1} in
		--auto-update)     (( AUTOUPDATE   = 1 )) ;;
		--no-auto-update)  (( AUTOUPDATE   = 0 )) ;;
		--colours)         (( COLOURS      = 1 )) ;;
		--no-colours)      (( COLOURS      = 0 )) ;;
		--hook-on-push)    (( HOOK_ON_PUSH = 1 )) ;;
		--no-hook-on-push) (( HOOK_ON_PUSH = 0 )) ;;
		--magic-etc)       (( MAGIC_ETC    = 1 )) ;;
		--no-magic-etc)    (( MAGIC_ETC    = 0 )) ;;
		--progress)        (( PROGRESS     = 1 )) ;;
		--no-progress)     (( PROGRESS     = 0 )) ;;
		--quiet)           (( SILENT       = 1 )) ;;
		--no-quiiet)       (( SILENT       = 0 )) ;;

		--version) die "see '${0} version'\n" ;;
		--help) die "see '${0} help'\n" ;;

		--packagedir)  PKG_DIR=${2}; shift ;;
		--checklinks-options) CL_OPTIONS+=${2}; shift ;;
		--) shift; break ;;
		*) die "Unknown argument: '${1}'\n" ;;
	esac
	shift
}

action=${1}
((#)) && shift

# If we already break BC we can at least give a warning ;)

if [[ -n ${PKG_ROOT} ]] {
	warn "There were major, BC-breaking changes to the format of caretaker.conf\n"
	warn "Please read caretaker.conf(5) and update it accordingly\n\n"
	warn "You'll probably also need to update your pkglist script, see\n" \
		"examples/pkglist in your caretaker repo\n"
	exit 1
}

if (( ${#PKG_ROOTS} == 0 )) {
	die "No package root(s) specified.\n" \
		"Please edit the PKG_ROOTS value in your config file\n"
}
: ${PKG_DIR:="${HOME}/packages"}
: ${CL_OPTIONS:=--quiet}
: ${SILENT=0}
: ${AUTOUPDATE=1}
: ${HOOK_ON_PUSH=1}
: ${COLOURS=1}
: ${MAGIC_ETC=1}
: ${PROGRESS=1}
export PKG_DIR
export PKG_PROTO PKG_USER PKG_HOST PKG_UAH PKG_PATH

if (( SILENT )) {
	GIT_SILENT_ARG=--quiet
} else {
	unset GIT_SILENT_ARG
}

self=${0}
self_path=${PKG_DIR}/${${(s:/:)$(readlink ${self})}[-3]}

if [[ ! -t 1 ]] {
	PROGRESS=0
	function clear_line {}
}

if (( SILENT )) {
	# The goal is not to override anything set by the user...
	# So, an alias should be safer than fiddling with ${MAKEFLAGS}
	alias make='make -s'
	function info say clear_line split_long {}
	PROGRESS=0
}

if (( ! COLOURS )) {
	unset c_info c_reset c_error
}

if [[ ! -d ${PKG_DIR} ]] {
	die "Error: Package directory '${PKG_DIR}' does not exist\n"
}

function pkgroot_setup {
	PKGLIST_LOCAL=0

	if [[ -n ${functions[pkgroot_${1}]} ]] {
		pkgroot_${1}
	} else {
		die "function pkgroot_${1} not defined in caretaker.conf\n" \
			"The config format was changed recently, please consult your" \
			"documentation ;-)\n"
	}

	: ${PKGLIST_PATH:=${PKG_PATH}/pkglist}

	if [[ -n ${PKG_USER} ]] {
		PKG_UAH="${PKG_USER}@${PKG_HOST}"
	} else {
		PKG_UAH=${PKG_HOST}
	}
}

function pkgroot_clean {
	unset PKG_PROTO PKG_HOST PKG_PATH PKG_USER PKG_UAH PKGLIST_PATH
}

function check_installed {
	if [[ -z ${1} || ! -d ${PKG_DIR}/${1} ]] {
		die "Package is not installed: '${1}'\n"
	}
}

function check_valid {
	if ! list_exists ${1}; then
		die "Unknown package name: '${1}' (not in package list)\n"
	fi
}

# Default reply: Yes
function confirm_yes {
	echo -n - "${*} [Y/n] "
	read -k 1
	[[ ${REPLY} != $'\n' ]] && echo
	[[ ${REPLY} == (y|Y|$'\n') ]]
}

# Default reply: No
function confirm_no {
	echo -n - "${*} [y/N] "
	read -q
}


##
## Major internal functions
##

function progress {
	(( PROGRESS )) || return

	typeset -i current=${1}
	typeset -i max=${2}
	typeset desc=${3}
	typeset desc2=${4}
	typeset output=''
	typeset -i currentper=$(( (current * 100) / max ))
	typeset -i item_current item_remain

	function item {
		repeat ${1}; {
			output+=${2}
		}
	}

	(( item_current = currentper / 5 ))
	(( item_remain  =  20 - item_current ))

	output+="${c_info}${desc}${c_reset} ["
	item ${item_current} '='
	item ${item_remain} ' '
	output+="] ${currentper}% ${desc2}"

	clear_line
	echo -ne ${output}
}

function split_long {
	while read line; do
		typeset -i line_length=0
		typeset -i first=1
		typeset buffer=
		for word in ${(s: :)line}; do
			word_length=${#word}

			if (( line_length + word_length > 80 )); then
				if [[ -n ${buffer} ]]; then
					echo ${buffer}
					buffer=
					line_length=0
					broke_line=1
				fi
			fi

			(( line_length += word_length + 1 ))

			if (( first )); then
				buffer=${word}
				first=0
			else
				buffer+=" ${word}"
			fi

		done
		echo ${buffer}
	done
}

## VCS Wrappers

function vcs_add (
	cd ${PKG_DIR}

	if [[ $(list_get_type ${1}) == git ]] {
		git clone $(list_get_uri ${1}) ${1}
		vcs_setup ${1}
		git config push.default matching
		vcs_update_submodules ${1}
	} else {
		 die "${1}: Cannot handle repository format '$(list_get_type ${1})'\n"
	}

	pkgroot_clean
)

function vcs_has_origin (
	[[ -e ${PKG_DIR}/${1}/.git/refs/remotes/origin ]]
)

function vcs_log (
	vcs_setup ${1}
	git log
)

function vcs_new (
	mkdir ${PKG_DIR}/${1}
	cd ${PKG_DIR}/${1} || die "Cannot cd to new package ${1}"
	git init

	git remote add origin ${PKG_PROTO}://${PKG_UAH}/${PKG_PATH}/${1}
)

function vcs_pull (
	vcs_setup ${1}
	git pull ${GIT_SILENT_ARG}
	vcs_update_submodules ${1}
)

function vcs_push (
	vcs_setup ${1}

	# Repos created with "ct new" don't have a remote tracking branch yet.
	# Detect this and fix it.

	if [[ $(git branch -r) == '' ]] {
		git push origin master
	} else {
		git push
	}
)

function vcs_setup {
	cd ${PKG_DIR}/${1} || die "vcs_setup: Cannot cd ${PKG_DIR}/${1}"
}

function vcs_status (
	typeset gitstatus
	vcs_setup ${1}
	git status --short
)

function vcs_to_list (
	vcs_setup ${1}
	if [[ -d ${PKG_DIR}/${1}/.git ]] {
		echo -n - "${1} git "
		echo ${$(git log -n 1 master)[2]}
	} else {
		warn "No git repository found: '${1}'\n"
	}
)

function vcs_update_submodules (
	vcs_setup ${1}
	if [[ -e .gitmodules ]] {
		git submodule update --init
	}
)

## List stuff

function list_exists {
	grep -q "^${1} " ${PKG_DIR}/.list-remote
}

function list_get_uri {
	echo - ${$(grep "^${1} " ${PKG_DIR}/.list-remote)[4]}
}

function list_get_type {
	echo - ${$(grep "^${1} " ${PKG_DIR}/.list-remote)[2]}
}

function list_get_type_local {
	echo - ${$(grep "^${1} " ${PKG_DIR}/.list)[2]}
}

function list_get_version_local {
	echo ${$(grep "^${1} " ${PKG_DIR}/.list)[3]}
}

function list_get_version_remote {
	echo ${$(grep "^${1} " ${PKG_DIR}/.list-remote)[3]}
}

function list_incoming {
	typeset local=$(list_get_version_local ${1})
	typeset remote=$(list_get_version_remote ${1})
	[[ -n ${local} && -n ${remote} && ${local} != ${remote} ]]
}

function list_is_installed {
	grep -q "^${1} " ${PKG_DIR}/.list
}

function list_package_update {
	typeset list
	list=$(grep -v "^${1} " ${PKG_DIR}/.list)
	echo - ${list} > ${PKG_DIR}/.list
	vcs_to_list ${1} >> ${PKG_DIR}/.list
}

function list_package_remove {
	typeset list
	list=$(grep -v "^${1} " ${PKG_DIR}/.list)
	echo - ${list} > ${PKG_DIR}/.list
}

function list_packages_local {
	print -l ${PKG_DIR}/*(:t)
}

function list_packages_remote {
	cut -d ' ' -f 1 ${PKG_DIR}/.list-remote
}

function list_update_local {
	typeset -i all=${#$(echo ${PKG_DIR}/*(/))}
	typeset -i current=0
	typeset package

	rm -f ${PKG_DIR}/.list

	for package in *(-/); {
		(( current++ ))
		progress ${current} ${all} 'Updating package list' ${package}
		vcs_to_list ${package} >> ${PKG_DIR}/.list
	}
	clear_line
}

function list_update_remote {
	typeset tmpfile=$(mktemp -t pkglist.XXXXXX) PKG_ROOT

	for PKG_ROOT in ${PKG_ROOTS}; {
		pkgroot_setup ${PKG_ROOT}

		if [[ ${PKGLIST_LOCAL} == 1 || ${PKG_PROTO} == 'file' ]] {
			${PKGLIST_PATH} >> ${tmpfile}
		} elif [[ ${PKG_PROTO} == 'ssh' ]] {
			ssh ${PKG_UAH} \
				"PKG_PATH=\"${PKG_PATH}\" PKG_UAH=\"${PKG_UAH}\"" \
				"PKG_PROTO=\"${PKG_PROTO}\"" \
				"${PKGLIST_PATH}" >> ${tmpfile}
		}

		pkgroot_clean
	}

	if [[ -n $(cat ${tmpfile}) ]] {
		cp ${tmpfile} .list-remote
		rm ${tmpfile}
	} else {
		rm ${tmpfile}
		die "remote list update failed\n"
	}
}


function priority_name {
	case ${1} in
		6) echo 'essential' ;;
		5) echo 'important' ;;
		4) echo 'required' ;;
		3) echo 'standard' ;;
		2) echo 'optional' ;;
		1) echo 'extra' ;;
		*) warn "invalid priority: '${1}'\n" ;;
	esac
}

function run_checklinks {
	if [[ -e links ]] {

		${self_path}/bin/checklinks ${*} ${CL_OPTIONS} \
			--parameter   package=${PWD}              \
			--parameter r_package=${PWD#${HOME}}      \
			--parameter   etc=${PWD}/etc              \
			--parameter r_etc=${PWD#${HOME}}/etc      \
			--parameter   pkgdir=${PKG_DIR}           \
			--parameter r_pkgdir=${PKG_DIR#${HOME}/}

	} elif [[ -d etc && ${MAGIC_ETC} == 1 ]] {
		${self_path}/bin/checklinks --ct-auto ${CL_OPTIONS} ${*}
	}
}

## Hook helper functions

function collect_into_directory {
	typeset directory=${1}
	shift

	info "collecting into ${directory}: ${*}\n" | split_long
	mkdir -p ${directory}
	rm -f ${directory}/*(@N)

	if [[ -e ${1} ]] {
		ln -s ${*} ${directory}
	}
}

function collect_into_file {
	typeset out=${1}
	shift

	info "collecting into ${out}: ${*}\n" | split_long
	cat /dev/null ${*} > ${out}
}

function exec_hook {
	typeset package=${1}
	typeset hook=${2}

	if [[ -r ${PKG_DIR}/${package}/hooks/${hook} ]] {
		info "${package}: executing hook ${hook}\n"
		cd ${PKG_DIR}/${package}
		(source hooks/${hook})
	}
}

function global_hook {
	cd ${PKG_DIR}/${1}

	case ${2} in
		post-add)
			exec_hook ${1} post-add
			global_hook ${1} post-update
		;;
		pre-update)
		;;
		post-update)
			triggers+=${1}
			check_prereqs ${1}
			if [[ -r Makefile ]] {
				info "${1}: running make\n"
				make
			}
			run_checklinks
			update_collected ${1}
			update_provides ${1}
			list_package_update ${1}
		;;
		pre-remove)
			exec_hook ${1} pre-remove
			genocide_collected ${1}
			run_checklinks --remove
			list_package_remove ${1}
			update_provides ${1}
		;;
	esac

	# execute custom hooks
	(( $+functions[pkg_hook_${2}] )) && pkg_hook_${2} ${1}
}

function check_prereqs {
	typeset -a -U force_install install maybe_install
	typeset warn info
	typeset package=${1}

	[[ -r ${PKG_DIR}/${package}/prereqs ]] || return 0
	cd ${PKG_DIR}/${package}
	info "${1}: checking prerequisites\n"

	function is_installed {
		[[ -d ${PKG_DIR}/${1} ]]
	}

	function perlmodule {
		perl -M${1} < /dev/null 2> /dev/null
	}

	function executable {
		which ${1} > /dev/null
	}

	function offer_install {
		install+=${1}
	}


	function force_depend {
		if [[ ${1} == 'package' ]] {
			is_installed ${2} || force_install+=${2}
		} else {
			warn "force_depend: Only makes sense for packages\n"
		}
	}

	function depend {
		if [[ ${1} == 'package' ]] {
			is_installed ${2} || offer_install ${2}
		} else {
			${*} || warn+="Requirement failed: ${*}\n"
		}
	}

	function suggest {
		if [[ ${1} == 'package' ]] {
			is_installed ${2} || info "${package} suggests package ${2}\n"
		} else {
			${*} || echo "Suggest failed: ${*}\n"
		}
	}

	function recommend {
		if [[ ${1} == 'package' ]] {
			is_installed ${2} || maybe_install+=${2}
		} else {
			${*} || info+="Recommend failed: ${*}\n"
		}
	}

	# function scope → typeset and localoptions are possible
	function {
		{
			source prereqs
		} always {
			if (( TRY_BLOCK_ERROR )) {
				warn "Error in prereqs script\n"
				TRY_BLOCK_ERROR=0
			}
		}
	}

	if [[ -n ${warn} || -n ${info} ]] {
		[[ -n ${warn} ]] && warn ${warn}
		[[ -n ${info} ]] && echo -n ${info}
		read -q '?continue [] '
	}

	if [[ -n ${force_install} ]] {
		info "${1} forces installation of additional packages: ${(j:,:)force_install}\n" \
		| split_long
		for package in ${force_install}; {
			pkg_add ${package}
		}
	}

	if [[ -n ${install} ]] {
		info "${1} requires the following packages: ${(j:, :)install}\n" \
		| split_long
		if confirm_yes "Install them?"; then
			for package in ${install}; {
				pkg_add ${package}
			}
		fi
	}

	if [[ -n ${maybe_install} ]] {
		info "${1} recommends the following packages: ${(j:,:)maybe_install}\n" \
		| split_long
		if confirm_no "Install them?"; then
			for package in ${maybe_install}; {
				pkg_add ${package}
			}
		fi
	}
}

# Write a package's documentation to .collected
# and symlink its binaries from ~/bin
function update_collected {
	cd ${PKG_DIR}/${1} || return
	typeset man section manpage file target

	if [[ ! -d bin && ! -d man ]] {
		return
	}

	info "${1}: processing documentation and binaries\n"

	for man in man/*/*(N); {
		section=${man:h:t}
		manpage=${man:t}
		target=${PKG_DIR}/.collected/man/man${section}/${manpage%.pod}.${section}

		if podchecker man/${section}/${manpage} &> /dev/null; then
			pod2man -u -s ${section} -c "${1} package" -r ${HOME} \
				man/${section}/${manpage} > ${target}
		elif [[ ${manpage} != *.pod && ! -e ${target} ]]; then
			ln -s ../../../${1}/man/${section}/${manpage} ${target}
		fi
	}

	for file in bin/*(N); {
		if podchecker ${file} &> /dev/null; then
			pod2man -u ${file} > ${PKG_DIR}/.collected/man/man1/${file:t}.1
		fi
	}

	for file in ~/bin/*(@N); {
		if [[ $(readlink ${file}) == \
			(../${PKG_DIR//${HOME}\/}|${PKG_DIR})/${1}/bin/${file:t} \
			&& ! -e ${PKG_DIR}/${1}/bin/${file:t} ]] \
		{
			rm ${file}
		}
	}

	for file in bin/*(-*N); {
		if [[ -L ${HOME}/${file} || ! -e ${HOME}/${file} ]] {
			if [[ $(readlink ${HOME}/${file}) != \
				(../${PKG_DIR//${HOME}\/}|${PKG_DIR})/${1}/${file} ]] \
			{
				rm -f ${HOME}/${file}
				if [[ ${PKG_DIR} == ${HOME}* ]] {
					ln -s ../${PKG_DIR//${HOME}\/}/${1}/${file} ${HOME}/${file}
				} else {
					ln -s ${PKG_DIR}/${1}/${file} ${HOME}/${file}
				}
			}
		} else {
			warn "update_collected: ~/${file} is not a symlink - skipping ${1}/${file}\n" \
			     "Please fix manually and then call '${0} refresh ${1}'\n"
		}
	}
}

# Remove a package's files from .collected
# Assuming there are no packages with colliding files
function genocide_collected {
	typeset file man manual section manfile

	cd ${PKG_DIR}/${1} || return
	if [[ ! -d bin && ! -d man ]] {
		return
	}

	info "${1}: removing documentation and binaries\n"

	for man in man/*/*(N); {
		section=${man:h:t}
		manual=${man:t}
		rm -f ${PKG_DIR}/.collected/man/man${section}/${manual%.pod}.${section}
	}

	for file in bin/*(N); {
		rm -f ${PKG_DIR}/.collected/man/man1/${file:t}.1
	}

	for file in bin/*(-*N); {
		if [[ $(readlink ${HOME}/${file}) == \
			(../${PKG_DIR//${HOME}\/}|${PKG_DIR})/${1}/${file} ]] \
		{
			rm -f ${HOME}/${file}
		}
	}
}

function update_provides {
	typeset package

	for package in ${PKG_DIR}/${1}/provides/*(N:t); {
		if [[ -d ${PKG_DIR}/${package} ]] {
			triggers+=${package}
		}
	}
}

function apply_triggers {
	typeset package

	for package in ${triggers}; {
		exec_hook ${package} 'post-update'
	}
}

# Iterate a function over every installed package
function wrap {
	typeset -i loop_all=0

	while [[ ${1} == -* ]] {
		case ${1} in
			-a) loop_all=1 ;;
		esac
		shift
	}

	typeset function=${1}
	typeset progress=${2}
	typeset package
	typeset -i all current
	typeset -a packages
	shift 2

	if [[ ${#} == 1 || ${loop_all} == 0 ]] {

		for package in ${*}; {
			cd ${PKG_DIR}
			${function} ${package}
		}

	} else {

		all=$(list_packages_local | wc -l)
		current=0

		for package in *(-/); {
			cd ${PKG_DIR}
			(( current++ ))
			progress ${current} ${all} ${progress} ${package}
			${function} ${package}
		}

		clear_line
	}
}

##
## The "frontend" functions
##

function pkg_add {
	check_valid ${1}
	if [[ -d ${PKG_DIR}/${1} ]] {
		die "Package '${1}' is already installed!\n"
	}

	info "${1}: retrieving package\n"
	vcs_add ${1} || return 255
	global_hook ${1} post-add
}

function pkg_debug {
	echo "--- running ---"
	echo "  zsh           ${ZSH_VERSION}"
	echo "  git           "${$(git --version)[3]}
	echo "  caretaker     "${$(git --git-dir=${self_path}/.git/ log -n 1)[2]}
	echo "--- settings ---"

	for PKG_ROOT in ${PKG_ROOTS}; {
		pkgroot_setup ${PKG_ROOT}
		echo "  PKG_ROOT      ${PKG_ROOT}"
		echo "    PKG_PROTO   ${PKG_PROTO}"
		echo "    PKG_USER    ${PKG_USER}"
		echo "    PKG_HOST    ${PKG_HOST}"
		echo "    PKG_UAH     ${PKG_UAH}"
		echo "    PKGLIST_PATH  ${PKGLIST_PATH}"
		echo "    PKGLIST_LOCAL ${PKGLIST_LOCAL}"
		pkgroot_clean
	}

	echo "  PKG_DIR       ${PKG_DIR}"
	echo "  CL_OPTIONS    ${CL_OPTIONS}"
	echo "  SILENT        ${SILENT}"
	echo "  COLOURS       ${COLOURS}"
	echo "  PROGRESS      ${PROGRESS}"
	echo "  AUTOUPDATE    ${AUTOUPDATE}"
	echo "  MAGIC_ETC     ${MAGIC_ETC}"
}

function pkg_help {
	echo "See 'man ${self:t}' or 'perldoc -F ${self_path}/man/1/ct.pod'"
}

function pkg_info {
	list_is_installed ${1} || list_exists ${1} || die "No such package: ${1}\n"

	typeset name=${1} package_root=${PKG_ROOT}
	typeset repo_type=$(list_get_type ${1})
	typeset priority priority_name
	typeset hooks makefile description state
	typeset uri

	list_exists ${1} && uri=$(list_get_uri ${1})

	if [[ -d ${1} ]] {
		cd ${1}

		if [[ -r priority ]] {
			priority=$(cat priority)
			priority_name=$(priority_name ${priority})
		}

		if [[ -d hooks ]] {
			hooks=$(ls -m hooks)
		}

		if [[ -r Makefile ]] {
			makefile=1
		}

		size=$(du -sh .$(list_get_type ${1}) | grep -o '.*[KMG]')

		if [[ -r description ]] {
			description=$(cat description)
		}

		state='installed'
		if list_incoming ${1}; then
			state+=', needs update'
		else
			state+=', up-to-date'
		fi

	} else {
		state='not installed'
	}

	function show_info {
		[[ -z ${2} ]] && return
		info "${1}: "
		echo ${2}
	}

	show_info 'Package' ${name}
	show_info 'Source' ${uri}
	show_info 'State' ${state}

	[[ -n ${priority} ]] && show_info 'Priority' "${priority} (${priority_name})"

	show_info 'Local Version' $(list_get_version_local ${1})
	show_info 'Remote Version' $(list_get_version_remote ${1})
	show_info 'Repository Type' ${repo_type}
	show_info 'Repository Size' ${size}
	show_info 'Hooks' ${hooks}
	show_info 'Description' ${description}
}

function pkg_list {
	typeset package

	case ${1} in
		''|local)
			list_packages_local
		;;
		all|remote)
			list_packages_remote
		;;
		not-installed)
			for package in $(list_packages_remote); {
				list_is_installed ${package} || echo ${package}
			}
		;;
	esac
}

function pkg_log {
	check_installed ${1}
	vcs_log ${1}
}

function pkg_new {

	package=${1}
	pd=${PKG_DIR}/${package}

	if [[ -z ${package} ]] {
		die "Usage: ct new <packagename>\n"
	}

	shift

	if list_exists ${package} && [[ ${#} == 0 ]] ; then
		die "We already have a package with this name: ${package}\n"
	fi

	pkgroot_setup ${PKG_ROOTS[1]}

	if ! list_exists ${package}; then
		info "Creating local package...\n"
		vcs_new ${package}

		if [[ ${PKG_PROTO} == ssh ]] {
			info "Creating remote package...\n"
			ssh ${PKG_UAH} "GIT_DIR=${PKG_PATH}/${package} git --bare init"
		}
	fi

	for file in ${*}; {

		if [[ ${file} != /* ]] {
			file=${HOME}/${file}
		}

		if [[ -x ${file} && ! -d ${file} ]]; then

			mkdir -p ${pd}/bin
			mv -i ${file} ${pd}/bin && \
			info "OK ${file} -> ${pd}/bin/${file:r}\n"

		elif [[ ${file} == ${HOME}/.* ]]; then

			mkdir -p ${pd}/etc
			mv -i ${file} ${pd}/etc/${file:t} && \
			info "OK ${file} -> ${pd}/etc/${file:t}\n"

			echo "soft ${file#${HOME}/} \$etc/${file:t}" >> ${pd}/links

		else

			say "Cannot handle ${file}"

		fi
	}
}

function pkg_push {
	check_installed ${1}

	if list_incoming ${1}; then
		clear_line
		info "${1}: pushing\n"

		if (( HOOK_ON_PUSH == 1 )) {
			global_hook ${1} pre-update
		}

		vcs_push ${1}

		if (( HOOK_ON_PUSH == 1 )) {
			global_hook ${1} post-update
		}
	fi
}

function pkg_refresh {
	check_installed ${1}
	clear_line
	global_hook ${1} pre-update
	global_hook ${1} post-update
}

function pkg_remove {
	check_installed ${1}

	if [[ -r ${PKG_DIR}/${1}/priority ]] {
		if (( $(cat ${PKG_DIR}/${1}/priority) > 3 )) {
			confirm_no "Package '${1}' is" \
			           $(priority_name $(cat ${PKG_DIR}/${1}/priority)). \
			           "Really remove?" || return
		}
	}

	global_hook ${1} pre-remove
	rm -rf ${PKG_DIR}/${1}
	info "${1} removed\n"
}

function pkg_status {
	typeset vcs_status
	check_installed ${1}

	vcs_status=$(PAGER='' vcs_status ${1})

	if [[ -n ${vcs_status} ]] {
		if ((SILENT)) {
			echo ${1}
		} else {
			clear_line
			info "${1}:\n"
			echo ${vcs_status}
		}
	}
}

function pkg_update {
	if [[ -z ${1} || ${1} == local ]] {
		info "Updating local package list\n"
		list_update_local
	}

	if [[ -z ${1} || ${1} == remote ]] {
		info "Updating remote package list\n"
		list_update_remote
	}
}

function pkg_upgrade {
	check_installed ${1}

	if list_exists ${1} && \
		[[ $(list_get_type ${1}) != $(list_get_type_local ${1}) ]]
	then
		clear_line
		warn "Incompatible systems. Please reinstall: ${1}\n" \
		     "  remote '$(list_get_type ${1})'" \
		     "<-> local '$(list_get_type_local ${1})'\n"
		return 1
	fi

	if list_incoming ${1}; then
		clear_line
		info "${1}: updating to $(list_get_version_remote ${1})\n"
		global_hook ${1} pre-update
		vcs_pull ${1}
		global_hook ${1} post-update
	fi
}

function pkg_version {
	print "${self:t} version" \
	      ${$(git --git-dir=${self_path}/.git/ log -n 1)[2]}
}

# cd ~/packages/caretaker; ct r .
# -> ct refresh caretaker

if [[ ${1} == . ]] {
	1=${${PWD#${PKG_DIR}/}%%/*}
}

cd ${PKG_DIR} || die "Cannot cd ${PKG_DIR}"

case ${action} in
	debug)   pkg_debug ${*} ;;
	e|eval)  eval      ${*} ;;
	help)    pkg_help       ;;
	i|info)  pkg_info  ${*} ;;
	ls|list) pkg_list  ${*} ;;
	l|log)   pkg_log   ${*} ;;
	n|new)   pkg_new   ${*} ;;
	version) pkg_version    ;;

	f|pull)
		(( AUTOUPDATE )) && pkg_update remote
		wrap -a pkg_upgrade 'Looking for updates' ${*}
	;;
	p|push)
		(( AUTOUPDATE )) && pkg_update
		wrap -a pkg_push 'Pushing' ${*}
	;;

	a|add)     wrap    pkg_add     'Adding' ${*}     ;;
	r|refresh) wrap -a pkg_refresh 'Refreshing' ${*} ;;
	rm|remove) wrap    pkg_remove  'Removing' ${*}   ;;
	s|status)  wrap -a pkg_status  'Checking package status' ${*} ;;
	u|update)          pkg_update   ${*} ;;

	*) die "wait, what?\nct: unknown action: '${action}'\n" ;;
esac

apply_triggers