#!/bin/sh

#
# bky - A minimalistic, distributed SCM / VCS
#
# Copyright (C) 2005/2006	  Angel Ortega <angel@triptico.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# http://www.triptico.com
#

VERSION="1.0.0-pre3"
OS=`uname`

# repository: by default, .bky
REPO=.bky

# -acC are mandatory
RSYNC_ARGS="-avcC --exclude=.bky --exclude=.bkyremote"

# diff args
DIFF_ARGS="-urN"

# all bky metadata to be excluded
BKY_EXCL="--exclude=.bky --exclude=.bkyfrom --exclude=.bkymsg --exclude=.bkyremote"

# current commit id
COMMIT="0"

[ -f .bkyrepo ] && REPO=`cat .bkyrepo`
[ -f .bkyfrom ] && COMMIT=`cat .bkyfrom`


do_list_changes()
# lists changed between commit ids $1 and $2
{
	# simulates (with -n) an rsync to store $1 into $2,
	# stripping rsync information cruft
	rsync -avCn --delete ${BKY_EXCL} $1 $2 | \
		awk 'BEGIN { getline } /^$/ { exit } { print $0 }'
}


do_commit()
# commits working directory into the repository, with a $COMMIT id
{
	# if there is a parent commit, take as base
	# for the hardlinking copy
	MORE_ARGS=""
	if [ -f .bkyfrom ] ; then
		MORE_ARGS="--link-dest=../`cat .bkyfrom`"
	fi

	rsync ${RSYNC_ARGS} ${MORE_ARGS} . ${REPO}/${COMMIT}
}


do_next_commit_id()
# calculates the next commit id
{
	# increments until the directory doesn't exist
	while [ -d ${REPO}/${COMMIT} ] ; do
		COMMIT=$(($COMMIT + 1))
	done
}


do_move_head()
# sets the HEAD to $COMMIT
{
	# no HEAD? just set it to the 0th commit
	if [ ! -f ${REPO}/HEAD ] ; then
		echo 0 > ${REPO}/HEAD
	else
		# if working directory's parent is HEAD,
		# set HEAD as the new one
		if [ "`cat .bkyfrom`" = "`cat ${REPO}/HEAD`" ] ; then
			echo "Moving HEAD..."
			echo $COMMIT > ${REPO}/HEAD
		else
			echo "Commit to non-HEAD: effective branching"
		fi
	fi
}


bky_update()
# update command: fills the current directory with commit id $1
{
	# if set as -A, update to HEAD; otherwise,
	# -r to the desired tag or commit id
	if [ "$1" = "-A" ] ; then
		TAG=`cat ${REPO}/HEAD`
	else
		[ "$1" = "-r" ] && TAG=$2
	fi

	if [ "$TAG" = "" ] ; then
		echo "Usage: bky update -A|-r {id or tag}"
		exit 1
	fi

	# fill
	rsync ${RSYNC_ARGS} --delete ${REPO}/${TAG}/ .

	# set TAG as parent
	echo $TAG > .bkyfrom
}


bky_diff()
# diff command
{
	DIFFORG=.
	DIFFDEST=${REPO}/`cat .bkyfrom`

	if [ "$1" != "" ] ; then

		DIFFDEST=${REPO}/$1

		if [ "$2" != "" ] ; then

			if [ "$2" = "-p" ] ; then
				DIFFORG=${DIFFDEST}/
				DIFFDEST=${REPO}/$(cat ${DIFFORG}/.bkyfrom)
			else
				DIFFORG=${REPO}/$2/
			fi
		fi
	fi

	# loops the desired changes
	for f in `do_list_changes ${DIFFORG} ${DIFFDEST}` ; do
		# if $f in both paths are not directories,
		# create a diff, stripping the .bky/NNN part
		if [ ! -d ${DIFFDEST}/$f ] && [ ! -d ${DIFFORG}/$f ] ; then
			diff -uN ${DIFFDEST}/$f ${DIFFORG}/$f 2> /dev/null | \
			sed -e "s|^--- .bky/[^/]*/*|--- ./| ; s|^+++ .bky/[^/]*/*|+++ ./|"
		fi
	done
}


bky_commit()
# commit command
{
	OP=$1
	shift

	case ${OP} in
	-n)
		# don't edit message
		;;

	-m)
		# message in command line
		echo $* > .bkymsg
		;;

	*)
		# default: ask for a message via editor
		TMPFILE=`mktemp ./bky.XXXXXX`

		touch ${TMPFILE}
		if [ -z "${EDITOR}" ]; then 
			echo "\$EDITOR environment variable is empty, use vi as default..."
			EDITOR=vi
		fi
		${EDITOR} ${TMPFILE}
		if [[ "$?" != 0 ]]; then
			echo "Error during editing or editor '$EDITOR' is not found!"
			echo "commit aborted."
			exit 0
		fi

		if [ -s ${TMPFILE} ] ; then
			mv ${TMPFILE} .bkymsg
		else
			rm -f ${TMPFILE}
			echo "Untouched commit message file; commit aborted."
			exit 0
		fi

		;;
	esac

	# calculates commit id
	do_next_commit_id

	# does it
	do_commit

	# moves head
	do_move_head

	# finally sets as current one
	echo $COMMIT > .bkyfrom
}


bky_init()
# init command: inits the repository
{
	if [ -d ${REPO} ] ; then
		echo "A repository in ${REPO} already exists, so I won't go on."
		echo "If you want to get rid of it, delete it and try again."
		exit 1
	fi

	mkdir -p ${REPO}
	bky_commit $*
}


# date_last_modified()
# Usage: date_last_modified file
# Prints the date file was last modified to stdout.  The date is in the
# form of "yyyy-mm-dd hh:mm" in 24 hour format.

date_last_modified()
{
    local file="$1"
    case $OS in
        *BSD)
            stat -t '%F %a %R' $file |
            sed -E -e 's/^[^"]+"[^"]+" "//' -e 's/".*//'
            ;;

        Linux)
            stat -c"%y" "$file" | cut -c1-16
            ;;
    esac
}


bky_log()
# log command: dumps log as Changelog
{
	# start from last commited version
	T=${COMMIT}

	# loop the commits from head to toes
	while [ -f ${REPO}/${T}/.bkymsg ] ; do

		# head of log entry: commit time
		COMMITDATE=`date_last_modified ${REPO}/${T}/.bkymsg`
		echo $COMMITDATE [${T}]
		echo

		# commit message, indented
		awk '{ print " ", $0 }' < ${REPO}/${T}/.bkymsg
		echo

		# jump to next
		if [ -f ${REPO}/${T}/.bkyfrom ] ; then
			T=`cat ${REPO}/${T}/.bkyfrom`
		else
			break
		fi
	done
}


bky_status()
# status command
{
	do_list_changes . ${REPO}/`cat .bkyfrom`
}


bky_tag()
# tags the parent commit id with tag
{
	TAG=$1

	if [ "$TAG" = "" ] ; then
		echo "Usage: bky tag {tag}"
		exit 1
	fi

	# if tag exists
	if [ -L ${REPO}/${TAG} ] ; then
		echo "Error: tag exists"
		exit 1
	fi

	ln -s `cat .bkyfrom` ${REPO}/${TAG}
}


bky_push_or_pull()
# pushes/pulls this repository to/from a remote site
{
	OP=$1
	REMOTE=$2

	if [ "${REMOTE}" = "" ] ; then
		if [ -f .bkyremote ] ; then
			REMOTE=`cat .bkyremote`
		else
			echo "No remote repository in command line nor one was"
			echo "set before; no further operation is possible"
			exit 1
		fi
	else
		# store repository for future use
		echo ${REMOTE} > .bkyremote
	fi

	case ${OP} in
	push)
		rsync -avzH --exclude=.bkyremote --delete . ${REMOTE}
		;;
	pull)
		rsync -avzH --exclude=.bkyremote --delete ${REMOTE}/ .
		;;
	*)
		echo "${OP}?"
		exit 1
	esac
	
}


bky_export()
# exports the HEAD without any bky metadata
{
	if [ "$1" = "" ] ; then
		echo "Usage: bky export {rsync destination}"
		exit 1
	fi

	rsync -av ${BKY_EXCL} ${REPO}/${COMMIT}/ $1
}


bky_patchset()
# returns a patchset, including the commit message
{
	DEST=$1

	if [ "${DEST}" = "" ] ; then
		echo "Usage: bky patchset {commit id}"
		exit 1
	fi

	if [ ! -d ${REPO}/${DEST} ] ; then
		echo "Invalid commit id ${DEST}"
		exit 1
	fi

	# dump commit message
	cat ${REPO}/${DEST}/.bkymsg
	echo

	# does parent exist?
	if [ -f ${REPO}/${DEST}/.bkyfrom ] ; then
		# get parent
		ORG=`cat ${REPO}/${DEST}/.bkyfrom`
	else
		# if the requested patchset has no parent (probably patchset #0),
		# create an empty, virtual commit id called START and diff with it
		mkdir -p ${REPO}/START
		ORG="START"
	fi

	bky_diff ${ORG} ${DEST}
}


###################################

# main

CMD=`basename $0`

if [ "$CMD" = "bky" ] ; then
	CMD=$1
	[ $# -gt 0 ] && shift
fi

case $CMD in
init|bky-init)
	bky_init $*
	;;

commit|bky-commit)
	bky_commit $*
	;;

log|bky-log)
	bky_log
	;;

update|bky-update)
	bky_update $1 $2
	;;

diff|bky-diff)
	bky_diff $1 $2
	;;

status|bky-status)
	bky_status
	;;

tag|bky-tag)
	bky_tag $1
	;;

push|bky-push)
	bky_push_or_pull push $1
	;;

pull|bky-pull)
	bky_push_or_pull pull $1
	;;

export|bky-export)
	bky_export $1
	;;

patchset|bky-patchset)
	bky_patchset $1
	;;

--version)
	echo ${VERSION}
	;;

*)
	echo "bky ${VERSION} - A minimalistic, distributed SCM / VCS"
	echo '(C) 2005 Angel Ortega <angel@triptico.com>'
	echo
	echo 'Usage: bky {command} [args]'
	echo
	echo 'Commands:'
	echo
	echo '  init [-m "commit msg" ]           Start a new repository'
	echo '  commit [-m "commit msg" ]         Commits changes'
	echo '  diff                              Show changes'
	echo '  diff {commit_id}                  Show changes from commit_id'
	echo '  diff {commit_id} -p               Show changes in commit_id from its parent'
	echo '  diff {commit_id} {commit_id}      Show changes between commit ids'
	echo '  status                            List changed files'
	echo '  log                               Dumps a changelog'
	echo '  update -r {commit_id}             Puts commit_id in the working dir'
	echo '  update -A                         Puts HEAD in the working dir'
	echo '  tag {tag}                         Tags a revision'
	echo '  pull [{repository}]               Pulls from repository'
	echo '  push [{repository}]               Pushes to repository'
	echo '  export {directory}                Exports to directory'
	echo '  patchset {commit_id}              Dumps a diff including the commit message'
	exit 1
esac

exit 0
