summaryrefslogtreecommitdiff
path: root/bin/ct
diff options
context:
space:
mode:
authorDaniel Friesel <derf@derf.homelinux.org>2009-07-11 01:17:18 +0200
committerDaniel Friesel <derf@derf.homelinux.org>2009-07-11 01:21:48 +0200
commit324568044af4287dd25a90dc143ea26df565d1e3 (patch)
tree0bcf9ff698d825c50a72d85c02560d88782e1915 /bin/ct
parent389bd0bcb530fee9c92ed9c419d82303a322274d (diff)
Renamed pkg to caretaker
Diffstat (limited to 'bin/ct')
-rwxr-xr-xbin/ct860
1 files changed, 860 insertions, 0 deletions
diff --git a/bin/ct b/bin/ct
new file mode 100755
index 0000000..ab216ad
--- /dev/null
+++ b/bin/ct
@@ -0,0 +1,860 @@
+#!/usr/bin/env zsh
+## caretaker - /home package manager and zsh playground
+## Copyright © 2008-2009 by Daniel Friesel <derf@derf.homelinux.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 CL_OPTIONS
+
+c_info=$'\e[0;36m'
+c_error=$'\e[0;31m'
+c_reset=$'\e[0m'
+
+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"
+}
+
+# Read local configuration
+: ${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
+} elif [[ -r $XDG_CONFIG_HOME/pkg/pkg.conf ]] {
+ warn "pkg was renamed to caretaker, so move your config to $XDG_CONFIG_HOME/caretaker/caretaker.conf"
+ source $XDG_CONFIG_HOME/pkg/pkg.conf
+} elif [[ -r $HOME/.pkg.conf ]] {
+ warn "pkg was renamed to caretaker, so move your config to $HOME/.caretaker.conf"
+ source $HOME/.pkg.conf
+}
+
+# Parse commandline options
+while [[ $1 == --* ]] {
+ case $1 in
+ --coluors) COLOURS=1 ;;
+ --no-colours) COLOURS=0 ;;
+ --quiet) SILENT=1 ;;
+ --no-quiiet) SILENT=0 ;;
+ --debug) DEBUG=1 ;;
+ --no-debug) DEBUG=0 ;;
+ --auto-update) AUTOUPDATE=1 ;;
+ --no-auto-update) AUTOUPDATE=0 ;;
+ --checklinks-options) CL_OPTIONS+=$2; shift ;;
+ --packagedir) PKG_DIR=$2; shift ;;
+ --packageroot) PKG_ROOT=$2; shift ;;
+ --progress) PROGRESS=1 ;;
+ --no-progress) PROGRESS=0 ;;
+ *) die "Unknown argument: '$1'\n" ;;
+ esac
+ shift
+}
+
+action=$1
+((#)) && shift
+
+[[ -n $PKG_ROOT ]] || die "No PKG_ROOT specified. Please edit your caretaker.conf\n"
+: ${PKG_DIR:="$HOME/packages"}
+: ${PKGLIST_LOCAL=0}
+: ${CL_OPTIONS:=--quiet}
+: ${SILENT=0}
+: ${DEBUG=0}
+: ${AUTOUPDATE=1}
+: ${GIT_USE_ORIGIN=1}
+: ${COLOURS=1}
+: ${PROGRESS=1}
+export PKG_DIR
+export PKG_ROOT
+
+function debug {
+ typeset func line
+ if (( $# >= 3 )) {
+ func=$1
+ line=$2
+ shift 2
+ }
+ echo "(debug) $func:$line: $*" >&2
+}
+
+# I need function name and line number of the function _calling_ debug,
+# so I can't get them from inside the debug function.
+alias debug='debug ${(z%):-%N %i}'
+
+# Avoid calling debug without debug mode... just in case one needs more speed
+if (( !DEBUG )) {
+ unalias debug
+ function debug {}
+}
+
+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 {}
+ function say {}
+ function clear_line {}
+ PROGRESS=0
+}
+
+if (( !COLOURS )) {
+ c_info=''
+ c_reset=''
+ c_error=''
+}
+
+if [[ ! -d $PKG_DIR ]] {
+ die "Error: Package directory '$PKG_DIR' not found\n"
+}
+
+
+##
+## Setup some additional variables related to PKG_ROOT
+##
+
+# Protocol
+if [[ $PKG_ROOT == ssh://* ]] {
+ PKG_PROTO='ssh'
+} elif [[ $PKG_ROOT == git://* ]] {
+ PKG_PROTO='git'
+} elif [[ $PKG_ROOT == /* ]] {
+ PKG_PROTO='file'
+} else {
+ die "Error: Unknown protocol in PKG_ROOT '$PKG_ROOT'\n"
+}
+
+# user, host, path
+if [[ $PKG_PROTO == (git|ssh) ]] {
+ PKG_HOST=${${PKG_ROOT#"${PKG_PROTO}://"}%%/*}
+ PKG_PATH=${PKG_ROOT#"${PKG_PROTO}://$PKG_HOST"}
+ if [[ $PKG_HOST == *@* ]] {
+ PKG_USER=${PKG_HOST%%@*}
+ PKG_HOST=${PKG_HOST#*@}
+ } else {
+ PKG_USER=$USERNAME
+ }
+} elif [[ $PKG_PROTO == 'file' ]] {
+ PKG_PATH=$PKG_ROOT
+}
+
+: ${PKGLIST_PATH:=$PKG_PATH/pkglist}
+
+if ((DEBUG)) {
+ info "caretaker: running in debug mode. Infos follow:\n"
+ echo "--- running ---"
+ echo " zsh $ZSH_VERSION"
+ echo " git "${$(git --version)[3]}
+ echo " caretaker "${$(git --git-dir=$PKG_DIR/${${(s:/:)$(readlink $0)}[-3]}/.git/ log -n 1)[2]}
+ echo "--- settings ---"
+ echo " PKG_ROOT $PKG_ROOT"
+ echo " PKG_PROTO $PKG_PROTO"
+ echo " PKG_USER $PKG_USER"
+ echo " PKG_HOST $PKG_HOST"
+ echo " PKGLIST_PATH $PKGLIST_PATH"
+ echo " PKGLIST_LOCAL $PKGLIST_LOCAL"
+ echo " PKG_DIR $PKG_DIR"
+ echo " CL_OPTIONS $CL_OPTIONS"
+ echo " SILENT $SILENT"
+ echo " COLOURS $COLOURS"
+ echo " PROGRESS $PROGRESS"
+ echo " AUTOUPDATE $AUTOUPDATE"
+ echo " GIT_USE_ORIGIN $GIT_USE_ORIGIN"
+}
+
+function check_installed {
+ [[ -n $1 && -d $PKG_DIR/$1 ]] || die "Package not installed: '$1'\n"
+}
+
+function check_valid {
+ list_exists $1 || die "Package does not exist: '$1'\n"
+}
+
+# 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
+##
+
+# this function only has content when wrap is used.
+# but since it's always called, it will be empty by default
+function wrap_info {}
+
+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 item j c a
+ function item {
+ for j in {0..$1}; {
+ (( j > 0 )) && output+=$2
+ }
+ }
+ c=$(( currentper/5 ))
+ a=$(( 20-c ))
+ output+="${c_info}$desc${c_reset} ["
+ item $c '='
+ item $a ' '
+ output+="] $currentper% $desc2"
+ clear_line
+ echo -ne $output
+}
+
+## VCS Wrappers
+
+function vcs_setup {
+ cd $PKG_DIR/$1
+}
+
+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_add (
+ if [[ $(list_type $1) == git ]] {
+ git clone "$PKG_ROOT/$1"
+ vcs_setup $1
+ git config push.default matching
+ } else {
+ die "$1: Cannot handle repository format '$(list_type $1)'\n"
+ }
+)
+
+function vcs_log (
+ vcs_setup $1
+ git log
+)
+
+function vcs_branch_is_master (
+ vcs_setup $1
+ typeset IFS=$'\n' branch line
+ for line in $(git branch); {
+ [[ $line == \*\ * ]] && branch=${line#* }
+ }
+ if [[ $branch != master ]] {
+ warn "$1: The currently checked out branch is not master, but '$branch'\n" \
+ "Currently, with GIT_USE_ORIGIN=0, caretaker can only operate on the branch master\n" \
+ " -> skipping repo, please fix manually\n"
+ return 1
+ }
+)
+
+function vcs_pull (
+ vcs_setup $1
+ if ((GIT_USE_ORIGIN)) {
+ # the package might be newly created and not have an origin yet
+ vcs_fix_origin $1
+ git pull
+ } else {
+ vcs_branch_is_master $1 && git pull $PKG_ROOT/${PWD:t} master
+ }
+)
+
+function vcs_push (
+ vcs_setup $1
+ if ((GIT_USE_ORIGIN)) {
+ # see above
+ vcs_fix_origin $1
+ git push
+ } else {
+ vcs_branch_is_master $1 && git push $PKG_ROOT/${PWD:t} master
+ }
+)
+
+function vcs_status (
+ vcs_setup $1
+ git status
+)
+
+function vcs_fix_origin (
+ vcs_setup $1
+ if [[ ! -r .git/remotes/origin && ! -r .git/refs/remotes/origin/HEAD ]] {
+ fgrep -q '[remote "origin"]' .git/config ||
+ git remote add origin $PKG_ROOT/$1
+ }
+)
+
+
+## List stuff
+
+function list_is_installed {
+ grep -q "^$1 " $PKG_DIR/.list
+}
+
+function list_exists {
+ grep -q "^$1 " $PKG_DIR/.list-remote
+}
+
+function list_packages_local {
+ cut -d ' ' -f 1 $PKG_DIR/.list
+}
+
+function list_packages_remote {
+ cut -d ' ' -f 1 $PKG_DIR/.list-remote
+}
+
+function list_incoming {
+ [[ $(list_version_local $1) != $(list_version_remote $1) ]]
+}
+
+function list_type {
+ echo ${$(grep "^$1 " $PKG_DIR/.list-remote)[2]}
+}
+
+function list_type_local {
+ echo ${$(grep "^$1 " $PKG_DIR/.list)[2]}
+}
+
+function list_update_remote {
+ typeset tmpfile=$(mktemp -t pkglist.XXXXXX)
+ typeset -i ret=0
+ export PKG_DIR
+ if [[ $PKGLIST_LOCAL == 1 || $PKG_PROTO == 'file' ]] {
+ $PKGLIST_PATH $PKG_PATH > $tmpfile
+ } elif [[ $PKG_PROTO == 'ssh' ]] {
+ ssh $PKG_USER@$PKG_HOST "PKG_DIR='$PKG_DIR' $PKGLIST_PATH $PKG_PATH" > $tmpfile
+ }
+ if [[ -n $(cat $tmpfile) ]] {
+ cp $tmpfile .list-remote
+ } else {
+ die "remote list update failed\n"
+ }
+ rm $tmpfile
+}
+
+function list_update_local {
+ typeset all=${#$(echo $PKG_DIR/*(/))}
+ typeset -i current=0
+ typeset i
+ rm -f $PKG_DIR/.list
+ for package in *(-/); {
+ (( current++ ))
+ progress $current $all 'Updating package list' $package
+ vcs_to_list $package >> $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_version_local {
+ echo ${$(grep "^$1 " $PKG_DIR/.list)[3]}
+}
+
+function list_version_remote {
+ echo ${$(grep "^$1 " $PKG_DIR/.list-remote)[3]}
+}
+
+
+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: $!" ;;
+ esac
+}
+
+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 ]] {
+ wrap_info $1
+ info "Running make\n"
+ make
+ }
+ checklinks $CL_OPTIONS \
+ --parameter package=${${PWD#$HOME}#/##} \
+ --parameter etc=${${PWD#$HOME}#/##}/etc
+ populate_collected $1
+ update_provides $1
+ list_package_update $1
+ ;;
+ pre-remove)
+ exec_hook $1 pre-remove
+ genocide_collected $1
+ checklinks $CL_OPTIONS --remove
+ list_package_remove $1
+ update_provides $1
+ ;;
+ esac
+ (( $+functions[pkg_hook_$2] )) && pkg_hook_$2 $1
+}
+
+function check_prereqs {
+ typeset -a -U install maybe_install
+ typeset warn info i
+ typeset package=$1
+ [[ -r $PKG_DIR/$package/prereqs ]] || return 0
+ cd $PKG_DIR/$package
+ wrap_info $1
+ info "checking prerequisites\n"
+
+ # function scope → typeset and localoptions are possible
+ function source_prereqs {
+ {
+ source prereqs
+ } always {
+ if (( TRY_BLOCK_ERROR )) {
+ warn "Error in prereqs script\n"
+ TRY_BLOCK_ERROR=0
+ }
+ }
+ }
+
+ function is_installed {
+ [[ -d $PKG_DIR/$1 ]]
+ }
+ function perlmodule {
+ perl -M$1 < /dev/null 2> /dev/null
+ }
+ function executable file_in_path {
+ if [[ $0 == file_in_path ]] {
+ warn "'file_in_path' is deprecated, use 'executable' instead\n"
+ }
+ which $1 > /dev/null
+ }
+ function offer_install {
+ install+=$1
+ }
+ function depend require {
+ if [[ $0 == require ]] {
+ warn "'require' is depracated, use 'depend' instead\n"
+ }
+ 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"
+ }
+ }
+
+ source_prereqs
+
+ if [[ -n $warn || -n $info ]] {
+ [[ -n $warn ]] && warn $warn
+ [[ -n $info ]] && echo -n $info
+ read -q '?continue [] '
+ }
+
+ if [[ -n $install ]] {
+ info "$1 requires the following packages: ${(j:, :)install}\n"
+ 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"
+ 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 populate_collected {
+ cd $PKG_DIR/$1 || return
+ typeset -i bins=0 bino=0 mans=0 mano=0
+ typeset man section manpage file
+
+ wrap_info $1
+ info "Enabling documentation\n"
+ for man in man/*/*(N); {
+ section=${man:h:t}
+ manpage=${man:t}
+ if podchecker man/$section/$manpage &> /dev/null; then
+ pod2man -u -s $section -c "$1 package" -r $HOME man/$section/$manpage > $PKG_DIR/.collected/man/man$section/${manpage%.pod}.$section
+ 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 [[ -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 "populate_collected: Not updating ~/$file since it's not a symlink\n"
+ }
+ }
+}
+
+# Remove a package's files from .collected
+# Assuming there are no packages with colliding files
+function genocide_collected {
+ typeset i file man manual section
+ cd $PKG_DIR/$1 || return
+ wrap_info $1
+ info "Removing documentation"
+ for man in man/*/*(N); {
+ section=${man:h:t}
+ manual=${man:t}
+ if [[ -e $PKG_DIR/.collected/man/man$section/$manual.$section ]] {
+ rm $PKG_DIR/.collected/man/man$section/$manual.$section
+ }
+ }
+ for file in bin/*(N); {
+ rm -f $PKG_DIR/.collected/man/man1/${file:t}.1
+ }
+ clear_line
+ 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 function=$1
+ typeset arg=$2
+ typeset progress=$3
+ typeset i
+ typeset -i all current
+
+ if [[ -n $2 ]] {
+ $function $2
+ } else {
+ function wrap_info {
+ clear_line
+ info "$1: "
+ }
+ [[ -n $progress ]] && all=$(list_packages_local | wc -l)
+ [[ -n $progress ]] && current=0
+ for package in *(-/); {
+ cd $PKG_DIR
+ (( current++ ))
+ [[ -n $progress ]] && progress $current $all $progress $package
+ $function $package
+ }
+ [[ -n $progress ]] && clear_line
+ }
+}
+
+##
+## The "frontend" functions
+##
+
+function pkg_add {
+ if [[ -d $PKG_DIR/$1 ]] {
+ info "Package '$1' is already installed!\n"
+ exit 1
+ }
+ check_valid $1
+ info "Retrieving package $1...\n"
+ vcs_add $1 || return 255
+ global_hook $1 post-add
+}
+
+function pkg_push {
+ check_installed $1
+ check_valid $1
+ if list_incoming $1; then
+ clear_line
+ info "Pushing $1\n"
+ global_hook $1 pre-update
+ vcs_push $1
+ global_hook $1 post-update
+ fi
+}
+
+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 "Package removed.\n"
+}
+
+function pkg_upgrade {
+ check_installed $1
+ check_valid $1
+ if [[ $(list_type $1) != $(list_type_local $1) ]] {
+ clear_line
+ warn "Incompatible systems. Please reinstall: $1\n"
+ warn " remote '$(list_type $1)' <-> local '$(list_type_local)'\n"
+ return 9
+ }
+ if list_incoming $1; then
+ clear_line
+ info "Updating $1 to $(list_version_remote $1)\n"
+ global_hook $1 pre-update
+ vcs_pull $1
+ global_hook $1 post-update
+ fi
+}
+
+function pkg_list {
+ typeset package crap
+ 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_status {
+ typeset vcs_status
+ check_installed $1
+ vcs_status=$(PAGER='' vcs_status $1)
+ if [[ -n $vcs_status && $vcs_status != *'nothing to commit (working directory clean)' ]] {
+ if ((SILENT)) {
+ echo $1
+ } else {
+ clear_line
+ info "$1:\n"
+ echo $vcs_status
+ }
+ }
+}
+
+function pkg_refresh {
+ check_installed $1
+ global_hook $1 pre-update
+ global_hook $1 post-update
+}
+
+function pkg_update {
+ if [[ -z $1 || $1 == local ]] {
+ info "Updating local package list\n"
+ list_update_local
+ clear_line
+ }
+ if [[ -z $1 || $1 == remote ]] {
+ info "Updating remote package list\n"
+ list_update_remote
+ }
+}
+
+function pkg_info {
+ check_valid $1
+
+ typeset name=$1
+ typeset repo_type=$(list_type $1)
+ typeset priority priority_name
+ typeset hooks makefile discription state
+ 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_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 'State' $state
+ [[ -n $priority ]] && show_info 'Priority' "$priority ($priority_name)"
+ show_info 'Local Version' $(list_version_local $1)
+ show_info 'Remote Version' $(list_version_remote $1)
+ show_info 'Repository Type' $repo_type
+ show_info 'Repository Size' $size
+ show_info 'Hooks' $hooks
+ show_info 'Description' $description
+}
+
+function pkg_log {
+ check_installed $1
+ vcs_log $1
+}
+
+
+cd $PKG_DIR || die "Cannot cd $PKG_DIR"
+
+# Note:
+# wrap foobar "$1" <- the "" are neccessary here, since $1 is optional (and therefore may be empty)
+case $action in
+ a|add|install)
+ if [[ $action == install ]] {
+ warn "ct install is deprecated, use ct add instead\n"
+ }
+ pkg_add $*
+ ;;
+ rm|delete|remove)
+ if [[ $action == delete ]] {
+ warn "ct delete is deprecated, use ct remove instead\n"
+ }
+ pkg_remove $*
+ ;;
+ i|info) pkg_info $* ;;
+ ls|list) pkg_list $* ;;
+ l|log) pkg_log $* ;;
+ p|push)
+ (( AUTOUPDATE )) && pkg_update
+ wrap pkg_push "$1" 'Pushing'
+ ;;
+ r|refresh) wrap pkg_refresh "$1" 'Refreshing' ;;
+ s|status) wrap pkg_status "$1" 'Checking package status' ;;
+ u|update) pkg_update $* ;;
+ f|upgrade|pull)
+ if [[ $action == upgrade ]] {
+ warn "ct updgrade is deprecated, use ct pull instead\n"
+ }
+ (( AUTOUPDATE )) && pkg_update remote
+ wrap pkg_upgrade "$1" 'Looking for updates'
+ ;;
+ e|eval) eval $* ;;
+ *) die "wait, what?\nct: unknown action: '$action'\n" ;;
+esac
+
+apply_triggers