#!/bin/sh

#
# Copyright (c) 2007, 2008 Oligem.com.  All rights reserved.
#
# This code is derived from software contributed to Oligem by
# Marc Vertes <mvertes@free.fr>.
#
# Permission to use, copy, modify, and 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.
#

# Distributed Item Manager

# This script is compatible with POSIX 1003.2 and 1003.2a conformant
# shells: *BSD /bin/sh, ash, bash*, *ksh*, zsh (in sh compatibility mode)
# and maybe others.
# It is not compatible with old bourne shell (i.e. Solaris /bin/sh).
# Adjust the first line of this script if necessary.

Dim_version=dim-0.5.2

# Coding conventions:
# - local variables start with lowercase.
# - Global variables start with uppercase, or '_'.
# - R1, R2, ... variables are used to return results in caller.

unset CDPATH
LC_ALL=C IFS='
	 '
export LC_ALL IFS

# Usage: getshell
# Output: R1=shell
# Returns the current shell interpreter version in a string.
# The first word is the shell name, the rest is the full version.
getshell()
{
	if [ "$BASH_VERSION" ]
	then
		R1="bash $BASH_VERSION"
	elif [ "$ZSH_VERSION" ]
	then
		R1="zsh $ZSH_VERSION"
	elif (typeset -n) >/dev/null 2>&1 # in a child, as ksh88 aborts
	then
		eval R1=\"ksh93 \${.sh.version}\"
	elif [ "$KSH_VERSION" ]
	then
		R1="pdksh $KSH_VERSION"
	elif [ "`{ : ${_z_?1}; } 2>&1`" = 1 ]
	then
		R1="sh ash/BSD"
	elif { echo 1 | read R1; [ "$R1" ]; };
	then
		R1="ksh88"
	else
		R1="unknown shell"
	fi
}

#
# Use obsolete syntax for compatibility with ancient shells.
#
Sys=`uname -s`
case "$Sys" in
Darwin)	Shell=`ps -p $$ -o command=` ;;
*)	Shell=`ps -p $$ -o args=` ;;
esac

# Shell interpreter portability init actions, to have a consistent
# behaviour accross different shells
getshell && Shellvers=$R1
case "$Shellvers" in
unknown\ shell)
        echo "The shell \"$Shell\" is probably too old and unsupported."
	echo "Please try another one (suggested: ksh93 or bash)."
	exit 1
	;;
bash\ *)
	set +o posix
	shopt -s expand_aliases
	;;
ksh93\ *)
	# substitute 'f()' by 'function f' in this script, then run.
	[ ! "$Ksh" ] && {
		awk '$1 ~ /^[_A-Za-z0-9]+\(\)$/ {
			$1 = "function " substr($1, 0, length($1) - 2)
		} {print}' `whence $0` >/tmp/${0##*/}.ksh.$$ &&
		Ksh=1 ${Shell%% *} /tmp/${0##*/}.ksh.$$ "$@"
		R1=$?
		rm -f /tmp/${0##*/}.ksh.$$
		exit $R1
	}
	unset Ksh
	alias local=typeset
	;;
pdksh\ *)
	alias local=typeset
	;;
ksh88)
	typeset command >/dev/null 2>&1 ||
	command() {
		typeset cmd=$1; shift
		[ "$cmd" = -v ] && { whence -p "$1"; return; }
		`whence -p $cmd` "$@"
	}
	not() { eval "$@" && return 1 || return 0; }
	alias local=typeset
	alias !=not
	;;
zsh\ *)
	setopt NO_BAD_PATTERN IGNORE_BRACES NO_NOMATCH SH_GLOB \
	       SH_OPTION_LETTERS SH_WORD_SPLIT SH_NULLCMD \
	       SH_FILE_EXPANSION BSD_ECHO GLOB_SUBST POSIX_IDENTIFIERS
	;;
esac
[ "$Shellvers" = ksh88 ] && set -o noclobber || set -C		# no clobber

# From that point we can use modern shell syntax.
Shell=${Shell%% *}

# Adjust tools and tool flags according to platform capabilities
case "$Sys" in
(SunOS|AIX)
	type nawk >/dev/null 2>&1 && alias awk=nawk
	type gfind >/dev/null 2>&1 && alias find=gfind
	type gxargs >/dev/null 2>&1 && alias xargs=gxargs
	type gtar >/dev/null 2>&1 && alias tar=gtar
	type gdiff >/dev/null 2>&1 && Diffcmd=gdiff
	;;
esac
Diffcmd=${Diffcmd:-diff}
diffcmd() { command $Diffcmd "$@"; }

# Set tools features
find . -maxdepth 0 -empty -cnewer . -print0 >/dev/null 2>&1 &&
Fempty=-empty Fcnewer=-cnewer Fprint0=-print0 X0=-0 ||
Fempty= Fcnewer=-newer Fprint0=-print X0=
diffcmd -L dummy /dev/null /dev/null >/dev/null 2>&1 && DiffL=1
diffcmd --changed-group-format=dummy /dev/null /dev/null >/dev/null 2>&1 &&
DiffC=1

# Usage: cmd cmd_name arg_syntax short_help long_help
# Add a command to the list of available dim user commands.
Cmds=" "	# global list of commands
cmd() { Cmds="$Cmds$1 "; eval Arg_$1=\$2 Stxt_$1=\$3 Ltxt_$1=\$4; }

cmd man '' 'Print dim manpage in txt2man(1) format'
man()
{
	echo 'NAME
  dim - a distributed version control system
SYNOPSIS
  dim [-aAbcefInpqsMSVv] [-C Config] [-D dir] [-U Url] [-o Optstr]
      [-B Vers] [-i File] [-0-9 [Vers2]] [Cmd [Arg ...]]
DESCRIPTION
  Dim is a version control system. It creates, clones, archives,
  lists, replicates, extracts, prints differences, merges, ensures
  authenticity and integrity of versions.
OPTIONS
  The options must be specified before command. Options not
  applying to a command are silently ignored.
  -a		Print all (anc, desc, version, clone, item).
  -A		Generate absolute archive (export).
  -b		Fork a new branch (save and export).
  -B Vers	Fork a new branch named Vers (save and export).
  -c		Report only files with unresolved conflicts (diff).
  -C Config	Set initial configuration file to Config instead of
		~/.dimrc.
  -D Dir	Use directory Dir, instead of default $PWD.
  -e		Select exported ancestor instead of local (diff, anc).
  -f		Force action.
  -i File	Include file File (diff).
  -I		Generate incremental archive (export).
  -M		Release in main branch instead of user branch (export).
  -N		Show added words only (wdiff).
  -O		Show removed words only (wdiff). If flags -N and -O are
		both set, wdiff shown only added and removed words, not
		unmodified content.
  -n		Dry run mode. Simulate actions but not perform them.
  -o Optstr	Set specific command options in string Optstr.
  -p		Print version path.
  -q		Quiet mode.
  -S 		Print version sha1 checksum, obtained from the version
		content.
  -s		Print short version name.
  -U Url	Use Url instead of default (get, put).
  -v		Verbose mode. Print detailed informations on /dev/stderr.
  -V 		Print the dim version and exit.
COMMANDS
'"$(for c in $(for c in $Cmds; do echo $c; done | sort)
  do
	man_cmd $c
  done)"'
ENVIRONMENT
  DIMRC		Pathname of dim configuration file. Default is 
		$HOME/.dimrc.
FILES
  $HOME/.dimrc  Initial script configuration file, in sh(1) format.
		It can be overriden by option -C Config or by
		environment variable DIMRC. This file must be
		accessible in read or write mode by owner only.
BUGS AND LIMITATIONS
  - Symbolic links, empty directories and special files are not
    supported yet.
  - Filenames with newlines are not supported yet.
  - MS-DOS format conversion (lf <-> crlf) is not supported yet.
  - There is no delta compression of binary files yet.
  - Passphrases for RSA signing are not supported.
  - Version logs and comments are not well managed yet.
  - The support of item composition is not yet implemented.
  - This manual page is still incomplete: lacks overall description,
    and examples.
  - Some documentation about dim internals is lacking for extension
    writers.
  - This list is too long.
SEE ALSO
  sh(1), dotty(1), dot(1)

  http://www.oligem.com/dim/
HISTORY
  Although born in late 2007, dim has its roots back into the early
  80s, and is the result of a long experience in configuration
  management for large industrial software development projects.
  It all started with P. Nicklin in Berkeley CSRG, who created
  SPMS, which was later derived into SPMS+ in Thomson-CSF by X.T.
  Ho and P. Bergheaud. Then, inspired by Plan 9, Bergheaud created
  IFS, which became SIM when it incorporated some concepts from
  Ho. On his side, Ho wrote an upper layer to SIM called SDL. In
  the 90s, M. Vertes integrated CVS and SPMS+ for Airsys-ATM, in
  a project called BTC. After some time, Vertes, later joined by
  Bergheaud, wrote a distributed and slim version of SIM called
  DVS, in use in Meiosys Inc, then IBM. dim is a complete redesign
  and rewrite of DVS, with new additional features, in portable
  shell.
AUTHOR
  Marc Vertes <mvertes@free.fr>'
}

# Usage: die [message ...]
die()
{
	[ "$1" ] && echo "dim fatal: $@" >&2
	if [ ! "$nousage" ]
	then
		eval arg=\$Arg_$Cmd stxt=\$Stxt_$Cmd ltxt=\$Ltxt_$Cmd
		if [ "$arg" -o "$stxt" ]
		then
			if [ "$Help" ]		# Long usage
			then
				printf "Usage: dim "
				man_cmd $Cmd
			else			# Short usage only
				printf "%-35s %s\n" "Usage: dim $Cmd $arg" \
				       "$stxt" >&2
			fi
		fi
	fi
	exit_actions
	exit 1
}

error() { echo "dim error: $@" >&2; }
warn() { echo "dim warning: $@" >&2; }

pusage()
{
	local c arg stxt
	printf 'Usage: dim command [options] [args]\nCommands:\n'
	for c in $Cmds
	do
		eval arg=\$Arg_$c stxt=\$Stxt_$c
		[ "$arg" -o "$stxt" ] &&
		printf "  %-37s %s\n" "$c $arg" "$stxt"
	done | sort
	echo 'Options:
  -q,-v,-f,-n,-h	Run flags: quiet, verbose, force, dry-run, help
  -s,-S,-p,-l   	Version output flags: short, checksum, path, long
  -a,-e,-<n>		Version select flags: all, export, n"th anc
  -b,-M,-B Vers 	Branch flags: force a branch, main, named
  -C Conf,-D Dir,-U Url	Override defaults
Arguments:
  Vers	Any of directory or file path, version, item-version, checksum
  Dir	Directory pathname
  Item  Item name'
}

# Usage: man_cmd cmd
man_cmd()
{
	local arg ltxt stxt
	eval arg=\$Arg_$1 ltxt=\$Ltxt_$1 stxt=\$Stxt_$1
	[ "$stxt" ] || return
	echo "$1 $arg   "
	echo "${ltxt:-	$stxt}"
}

# Usage: lget file
# Output: R1
# Read the content of file in var list. 
lget() { [ -f "$1" ] && read -r R1 <$1; }

# Usage: lin list var
# return 0 (true) if var is present in list, 1 (false) otherwise
lin() { case " $1 " in (*" $2 "*) return 0;; (*) return 1;; esac; }

# Usage: ladd file var...
# Append list of var to the existing list in file
# Do not add an already present var
ladd()
{
	local f=$1 i l
	shift
	[ -f "$f" ] && read -r l <$f || l=
	for i
	do
		lin "$l" $i || l="$l $i"
	done
	echo $l >|$f
}

# Usage: ldel file var...
# Remove list of var from list of elements in file
ldel()
{
	local f=$1 e i l res
	shift
	[ -f "$f" ] || return
	read -r l <$f
	for e in $l
	do
		for i
		do
			[ "$e" = "$i" ] && continue 2
		done
		res="$res $e"
	done
	echo $res >|$f
}

cmd dag '[Item|Vers [Vers2]]' 'Print ancestor graph in dot format' \
'	Print an ancestor-descendant directed acyclic graph (dag)
	in a format usable by dot(1) or dotty(1). The graph is
	rooted at the most ancestor node, by default 0, in the top,
	and displays all existing nodes in descending order. If
	version Vers is specified, then the graph is rooted at it.
	If version Vers2 is specified, all nodes found after Vers2
	are skipped. In the produced graph, a node represents an
	item version, a black solid edges represents the
	ancestor-descendant relationship, from ancestor to descendant.
	A red dotted edge represents the ancestor2 relationship,
	induced by merge. The current clone node, if found, is
	diplayed in grey.'
# FIXME: set distinct attributes for global, local and clone versions
dag()
{
	local a=0 aa e d d2 v item nclone
	if [ "$1" -o "$N1" -o "$OptM" -o "$Opte" ]
	then
		getiv "$1" && item=$R1 a=$R2
		[ "$a" ] && Narg=$N1 getanc $item $a && a=$R1
		a=${a:-0}
	fi
	if [ "$2" -o "$N2" ]
	then
		getiv "${2:-$1}" && e=$R2 || die "invalid version"
		Narg=$N2 getanc $item $e && e=$R1
	fi
	getiv && isclone "$R2" && item=$R1 nclone=${R2#+}
	[ "$item" ] || die "no item"
	{ echo "digraph $item {"
	while [ "$a" ]
	do
		for v in $a
		do
			[ "$v" != "+$nclone" ] && echo "\"$v\";" ||
			echo "\"$v\" [color=lightgrey style=filled ];"
			d=
			lget "$Job/$item/.dim/$v/desc" && d=$R1 &&
			for vv in $d
			do
				echo "\"$v\" -> \"$vv\";"
			done
			lget "$Job/$item/.dim/$v/desc2" && d2=$R1 &&
			for vv in $d2
			do
				[ "$v" = "$vv" ] && continue
				[ "$vv" = "+$nclone" ] && 
				echo "\"$vv\" [color=lightgrey style=filled ];"
				echo "\"$v\" -> \"$vv\" [style=dotted color=red];"
			done
			[ "$d" ] || continue
			[ "$e" ] && lin "$d $d2" $e && break2
			aa="$aa $d"
		done
		a=$aa aa=
	done | sort -u
	echo "}"; } |
	if [ -t 1 ] && [ "$DISPLAY" ] && type dotty >/dev/null 2>&1
	then
		dotty -
	else
		cat
	fi
}

# Usage: getiv string
# Output: R1=item R2=vers R3=root R4=entry
# Find the version matching string, and compute result.
# String can be a relative or absolute path, a checksum, a symbolic
# version name, prefixed or not with the item name.
# Return item, version, root, entry
# If no match is found, no result is set and 1 is returned.
getiv()
{
	local item vers entry dir root n d l
	[ "$1" ] || set .
	case $1 in
	(*:*)		# itemvers:entry
		l=${1%:*} entry=${1#*:}
		entry=${entry#./}	# FIXME: canonify entry
		getiv "$l" && item=$R1 vers=$R2 root=$R3 
		[ "$vers" ] && gawk -v entry="$entry" '
		substr($0, 44) == entry { found = 1; exit 0 }
		END { exit 1 - found }' $Job/$item/.dim/$vers/index || entry=
		;;
	(*/*|.|..)	# path
		getitem "$1" && item=$R1 n=$R2 d=$R3
		if [ "$n" ] 
		then
			vers=+$n
		else
			case "$d" in
			($Job/*/.dim/*)
				vers=${d##*/}
				;;
			($Job/*/*)
				vers=${d#$Job/*/*-}
				vers=${vers%%/*}
				;;
			esac
		fi
		;;
	([a-zA-Z]*)		# itemversion
		item=${1%%[-+]*}
		if [ -d "$Job/$item" ]
		then
			[ -d "$Job/$item/$1" ] && vers=${1#$item}
			vers=${vers#-}
		else
			item=
		fi
		;;
	([0-9]*|+[0-9]*)	# version
		getiv && item=$R1
		[ -d "$Job/$item/.dim/$1" ] && vers=$1
		;;
	(*)			# invalid id
		error "invalid id: $1"
		;;
	esac
	R1=$item R2=$vers R3=$d R4=$entry
}

# Usage: pversion item vers
pversion() { getpvers "$1" "$2" && echo "$R1"; }

# Usage: getpvers item vers
# Output: R1=pvers
# Set a version string according to specified command line options.
getpvers()
{
	[ "$2" ] || return
	if [ "$Opts" ]		# Short form
	then
		R1=$2
	elif [ "$OptS" ]	# Checksum form
	then
		read R1 <$Job/$1/.dim/$2/sum
	elif [ "$Optp" ]	# Pathname form
	then
		case $2 in
		(+*)	R1=$Job/$1/$1$2 ;;
		(*)	R1=$Job/$1/$1-$2 ;;
		esac
	else			# Default form: item-vers
		case $2 in
		(+*)	R1=$1$2 ;;
		(*)	R1=$1-$2 ;;
		esac
	fi
}

# Usage: getdir path 
# Ouptut: R1=dir R2=file
getdir()
{
	local odir=$PWD f
	[ "$1" ] || set .
	# If $1 is a file, replace it by its directory
	if [ -f "$1" ]
	then
		if [ "${1%/*}" = "$1" ]
		then
			f=$1
			set -- . $2 $3
		else
			f=${1##*/}
			set -- ${1%/*} $2 $3
		fi
	fi
	cd "$1" 2>/dev/null && R1=$PWD R2=$f && cd "$odir" && return
	return 1
}

# Usage: getanc item vers
# Output: R1=anc
# Internal use only. Vers is the item version in short form.
getanc()
{
	local i tmp ltmp
	[ "$2" = 0 ] && { R1=0; return; }
	lget "$Job/$1/.dim/$2/anc" && tmp=$R1 || die "no anc: $2"
	[ "$Opte" ] && tmp=${tmp%%--*}
	[ "$OptM" ] && tmp=${tmp%%-*}
	[ "$Narg" ] &&
	if [ $Narg -gt 1 ]
	then
		i=1
		while [ $i -lt $Narg ]
		do
			lget "$Job/$1/.dim/$tmp/anc" && tmp=$R1
			i=$(($i + 1))
		done
	elif [ $Narg -eq 0 ]
	then
		tmp=$2
	fi
	if [ ! "$Opta" ]
	then
		R1=$tmp
		return
	fi
	ltmp=$tmp
	while [ "$tmp" -a "$tmp" != 0 ]
	do
		lget "$Job/$1/.dim/$tmp/anc" && tmp=$R1 || break
		ltmp="$ltmp $tmp"
	done
	R1=$ltmp
}

cmd anc '[-aeHlpSs] [Vers [Vers2]]' 'Print the ancestor(s) of Vers' \
'	Print the ancestor of Vers. If Vers2 is specified, print
	the closest common ancestor of Vers and Vers2. If -e flag
	is set, print the exported ancestor. If -a flag is set, print
	all ancestors, from recent to old, up to origin 0. If -H flag
	is set, print all original ancestors, including removed or not
	importes ones. If -l flag is set, print local ancestors from
	last exported version.'
anc()
{
	local v v1 v2 item
	getiv "$1" && item=$R1 v1=$R2
	[ "$v1" ] || die "invalid version: ${1:-.}"
	if [ $# = 2 ]
	then
		getiv "$2" && v2=$R2
		[ "$R1" != "$item" -o "$v2" = "" ] && die "invalid version: $2"
		getcanc $item $v1 $v2 && pversion $item $R1
	else
		if [ "$Optl" ]
		then
			getlocalanc $item $v1
		elif [ "$OptH" ]
		then
			gethist $item $v1
		else
			Narg=${N1:-1} getanc $item $v1
		fi
		for v in $R1
		do
			pversion $item $v
		done
	fi
}

cmd anc2
anc2()
{
	local v a a2 stop item a1
	getiv "$1" && item=$R1 v=$R2
	[ "$v" ] || die "invalid version: ${1:-.}"
	if [ "$2" ]
	then
		getiv "$2" && stop=$R2
		[ "$R1" != "$item" -o "$stop" = "" ] &&
		die "invalid version: $2"
	fi
	if [ "$OptH" ]
	then
		gethist2 $item $v && a2=$R1
	else
		Opta=1 getanc $item $v && a1=$R1
		getclosure anc $item $v && a2=$R1
	fi
	for a in $a2
	do
		[ "$Optl" ] && isexp $a && continue
		[ "$Opte" ] && ! isexp $a && continue
		lin "$a1" $a && continue
		[ "$a" = "$v" ] && continue
		pversion $item $a
	done
}

# Usage: getanc2 item vers
getanc2() { lget "$Job/$1/.dim/$1/anc2"; }

# Usage: addanc2 item vers1 vers2
# Add vers2 to anc2 list of item vers1
addanc2()
{
	local a2 v
	# Do not proceed if versions are not present
	[ -d "$Job/$1/.dim/$2" -a -d "$Job/$1/.dim/$3" ] || return
	# Do not add a version which is already our ancestor.
	isanc2 $1 $3 $2 && return
	getanc2 $1 $2 && a2=$R1
	for v in $a2
	do	# Remove redundant arcs
		isanc2 $1 $v $3 && rmanc2 $1 $2 $v
	done
	ladd "$Job/$1/.dim/$2/anc2" $3
	ladd "$Job/$1/.dim/$3/desc2" $2
}

# Usage: rmanc2 item vers1 vers2
rmanc2()
{
	ldel "$Job/$1/.dim/$2/anc2" $3
	ldel "$Job/$1/.dim/$3/desc2" $2
}

# Usage: gethist item vers
# Output: R1=history_ancestor_list
# Return the list of original ancestors, including removed ones
gethist()
{
	local h v=$2
	while true
	do
		lget "$Job/$1/.dim/$v/hist" && h="$h $R1" && break
		lget "$Job/$1/.dim/$v/anc" || break
		h="$h $R1" v=$R1
	done
	R1=$h
}

# Usage: gethist2 item vers
# Output: R1=history_ancestor2_list
gethist2()
{
	local more=$2 new list res res2 next i j hist
	while [ "$more" ]
	do
		new=$more more=
		for i in $new
		do
			list=
			lget "$Job/$1/.dim/$i/hist" && list=$R1 ||
			lget "$Job/$1/.dim/$i/anc" && list=$R1
			lget "$Job/$1/.dim/$i/hist2" && list="$list $R1" ||
			lget "$Job/$1/.dim/$i/anc2" && list="$list $R1"
			for j in $list
			do
				lin "$res" $j || res="$res $j" more="$more $j"
			done
		done
	done
	gethist $item $vers && hist=$R1
	for i in $res
	do
		isexp $i || continue
		lin "$hist" $i && continue
		res2="$res2 $i"
	done
	R1=$res2
}

cmd desc '[-aepSs] [Vers [Vers2]]' 'Print descendants of Vers'
desc()
{
	local item vers v2 ld v vv
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version: ${1:-.}"
	[ "$2" ] && {
		getiv "$1" && v2=$R2
		[ "$item" != "$R1" -o "$v2" = "" ] && die "invalid version: $2"
	}
	while [ "$vers" ]
	do
		ld=
		for v in $vers
		do
			desc=
			lget "$Job/$item/.dim/$v/desc" && desc=$R1 &&
			for vv in $desc
			do
				[ "$v2" = "$vv" ] && continue
				[ "$v2" ] && {
					isanc2 $item $vv $v2 || continue
				}
				[ "$Opte" -a ${vv%--*} != $vv ] && continue
				pversion $item $vv
			done
			ld="$ld $desc"
		done
		[ "$Opta" ] && vers=$ld || vers=
	done
}

# Usage istextfile file
# Return 0 if file is a text file
istextfile()
{
	[ -f "$1" -a ! -s "$1" ]  && return
	diffcmd /dev/null "$1" | awk '/differ/ {exit 1} {exit 0}'
}

# Usage: urlenc
# Encode stdin to be passed in HTTP request. The encoding consists to
# replace each non alphanum character by '%0', then char ASCII value in hexa.
urlenc()
{
	awk 'BEGIN {
		for (i = 1; i <= 255; i++)
			ord[sprintf("%c", i)] = i
	}
	NR > 1 {printf "%s", "%0A"}	# Encode newline
	{
		enc = ""
		for (i = 1; i <= length($0); i++) {
			c = substr($0, i, 1)
			if (c ~ /[a-zA-Z0-9._-]/)
				enc = enc c
			else if (c == " ")
				enc = enc "+"
			else
				enc = enc "%" sprintf("%02X", ord[c])
		}
		printf "%s", enc
	}
	END {print ""}'
}

# Usage: aar Vers
# Create an absolute archive of Vers, sent on stdout.
aar()
{
	local anc item=$1 vers=$2
	local tmpd=${Job%/*}/aar.$$
	local la la2 nla2 v

	lget "$Job/$item/.dim/$vers/anc" && anc=$R1 || die "no ancestor"
	trap "rm -rf \"$tmpd\"" EXIT
	mkdir -p "$tmpd/cat/meta" "$tmpd/cat/data"
	cd "$Job/$item/.dim/$vers"
	ln -f anc index log sum date from sign "$tmpd/cat/meta"
	getexportedanc2 $item $vers
	[ "$R1" ] && echo $R1 >$tmpd/cat/meta/anc2
	gethist $item $vers && echo $R1 >$tmpd/cat/meta/hist
	gethist2 $item $vers && echo $R1 >$tmpd/cat/meta/hist2
	lnd "$Job/$item/$item-$vers" "$tmpd/cat/data" "$tmpd/cat/meta/index"
	cd "$tmpd" && $Ar_cmd *
}

# Usage: dar item vers
# Create a delta archive, sent on stdout
dar()
{
	local item=$1 vers=$2 anc tmpd=${Job%/*}/dar.$$
	lget "$Job/$item/.dim/$vers/anc" && anc=$R1 || die "no ancestor"
	trap "rm -rf \"$tmpd\"" EXIT
	# diff relevant metadata
	cd "$Job/$item/.dim/$vers"
	mkdir -p "$tmpd/ned/meta" "$tmpd/cat/meta"
	cp anc log sum date from sign $tmpd/cat/meta 2>/dev/null
	# add exported anc2 to archive, instead of local ones
	getexportedanc2 $item $vers
	[ "$R1" ] && echo $R1 >$tmpd/cat/meta/anc2
	[ "$la2" ] && echo $la2 >$tmpd/cat/meta/anc2
	diffcmd -n ../$anc/index index >$tmpd/ned/meta/index

	# diff data
	mkdir -p "$tmpd/ned/data" "$tmpd/cat/data"
	cd "$Job/$item/$item-$vers"
	awk -v f1=../.dim/$anc/index -v f2=../.dim/$vers/index \
	    -v ddir="$tmpd/cat/data" -v cpio="cpio -pdul " '
	BEGIN {
		ln_cmd = cpio ddir " 2>/dev/null"	# file link coprocess
		while (getline line <f1 > 0)
			c1[substr(line, 44)] = substr(line, 1, 42)
		while (getline line <f2 > 0)
			c2[substr(line, 44)] = substr(line, 1, 42)
		for (f in c1)
			if ((f in c2) && (c1[f] != c2[f]))	# change
				print f | ln_cmd
		for (f in c2)
			if (! (f in c1))	# creation
				print f | ln_cmd 
		close(ln_cmd)	# to avoid race with post processing
		for (f in c1)
			if ((f in c2) && (c1[f] != c2[f]))	# change
				print f		# -> file_delta
	}' |
	while read file		# Compute individual file delta
	do
		dfile=$tmpd/ned/data/$file cfile=$tmpd/cat/data/$file
		ddir=${dfile%/*}
		[ -d "$ddir" ] || mkdir -p "$ddir"
		diffcmd -n "../$item-$anc/$file" "$file" >$dfile
		read -r res <$dfile
		case "$res" in
		(*" differ"*) rm "$dfile" ;;	# suppress delta
		(*)	      rm "$cfile" ;;	# keep data delta
		esac
	done
	cd "$tmpd" && $Ar_cmd *
}

# Usage: http_getnewvers item sum anc
# Output: R1=vers
http_getnewvers()
{
	[ "$Email" ] || die "no Email in $DIMRC"
	R1=$(Http_data="mail=$(printf %s $Email | urlenc)&p=$(printf %s $2 | sign2)" wget "$Url?nv=$2&item=$1&anc=$3&vn=${Branch:-.}&b=${Optb:-0}&n=${Optn:-0}&M=${OptM:-0}")
	case $R1 in (*[!A-Za-z0-9._-]*) die "server response: $R1" ;; esac
}

# Usage: file_getnewvers item sum anc
# Output: R1=vers
file_getnewvers()
{
	local item=$1 furl=${Url#file:} overwrite b sfile
	sfile=$furl/item/$item/.sum/$2
	lget "$sfile" &&
	case $R1 in
	(*--*)	;;			# ignore saved local versions
	(*)	return ;;		# return existing version
	esac
	[ -f "${Job%/*}/user/$Email" ] ||
	nousage=1 die "Please run \"dim mkuser\" first."
	. "${Job%/*}/user/$Email"
	getnewvers $item $2 && [ "$Optn" ] && return
	[ -d "$furl/item/$item/.sum" ] ||
	mkdir -p "$furl/item/$item/.sum" || nousage=1 die
	[ -d "$furl/item/$item/.version" ] ||
	mkdir "$furl/item/$item/.version" || nousage=1 die
	# Automatic branch creation if name collision
	while true
	do
		echo $2 >$furl/item/$item/.version/$R1 && break
		b=${R1##*[!0-9]}		# get last digits
		R1=${R1%$b}$(($b - 1)).1	# fork a branch
	done
	echo $R1 >|$sfile
}

# Usage: rsync_getnewvers item sum anc
# Output: R1=vers
rsync_getnewvers()
{
	local item=$1 b res ores nsum=
	[ -f "${Job%/*}/user/$Email" ] || 
	nousage=1 die "Please run \"dim mkuser\" first."
	. "${Job%/*}/user/$Email"
	cd "${Job%/*}"
	getnewvers $item $3 && res=$R1
	if [ "$Optn" ]
	then
		rsync -R --ignore-existing item/$item/.sum/$Zerosum \
		      "${Url#rsync:}" || nousage=1 die
	else
		ores=$res
		echo $res >|item/$item/.sum/$2
		rsync -R --ignore-existing item/$item/.sum/$2 "${Url#rsync:}" ||
		nousage=1 die
	fi
	rsync -rd "${Url#rsync:}/item/$item/.sum" item/$item/ || nousage=1 die
	lget "item/$item/.sum/$2" && [ "$R1" = "$res" ] || return 0

	while true
	do
		while true
		do
			echo $2 >item/$item/.version/$res 2>/dev/null && break
			b=${res##*[!0-9]}		# get last digits
			res=${res%$b}$(($b - 1)).1	# fork a branch
		done
		rsync -R --ignore-existing item/$item/.version/$res \ "${Url#rsync:}" ||
		nousage=1 die "rsync failed"
		rsync "${Url#rsync:}/item/$item/.version/$res" \
		      item/$item/.version/$res || nousage=1 die "rsync failed"
		lget "item/$item/.version/$res" && nsum=$R1
		[ "$2" = $nsum ] && break
		rm item/$item/.version/$res 
		b=${res##*[!0-9]}		# get last digits
		res=${res%$b}$(($b - 1)).1	# fork a branch
	done
	[ "$ores" = "$res" ] || {
		echo $res >|item/$item/.sum/$2
		rsync -R item/$item/.sum/$2 "${Url#rsync:}"  ||
		nousage=1 die "rsync failed"
	}
	R1=$res
}

cmd export '[-AIMbnqv] [-B Vers] [Vers]' 'Export a public version' \
'	Export a version Vers of item to a public repository. A
	unique global version name is returned.'
do_export()
{
	local item ivers elog lvers sfile sum xanc vers anc xanc d desc \
	      lanc2 v ldesc2 arcmd arname
	getiv "$1" && item=$R1 ivers=$R2
	[ "$ivers" ] || die "invalid version: $1"

	# aggregate log of local versions since last export
	# FIXME: to be corrected.
	elog=$(Opte=1 log $ivers)

	# Save a clone version before exporting it
	lvers=$(Cmd=save Opts=1 Optb= OptM= save $ivers)
	[ "$lvers" ] && isexp $lvers && echo $item-$lvers && return

	[ "$Optn" ] && sfile=$Job/$item/.dim/$ivers/sum ||
	sfile=$Job/$item/.dim/$lvers/sum
	lget "$sfile" && sum=$R1 || die "no sum"

	# Obtain a version name from the server
	xanc=${lvers%--*}
	case "$Url" in
	(http:*)	http_getnewvers $item $sum $xanc && vers=$R1 ;;
	(file:*)	file_getnewvers $item $sum $xanc && vers=$R1 ;;
	(rsync:*)	rsync_getnewvers $item $sum $xanc && vers=$R1 ;;
	esac
	[ "$vers" ] || return
	[ "$Optv" ] && [ "$lvers" != "$vers" ] && 
	echo "rename $item-$lvers to $item-$vers" >&2

	# By default, generate absolute archive for main, or incremental
 	if ismain $vers && [ ! "$OptI" ] || [ "$OptA" ]
	then
		arcmd=aar arname="$Archive/$item/$item-$vers.$Ar_ext"
	else
		arcmd=dar arname="$Archive/$item/$item-$vers--$xanc.$Ar_ext"
	fi

	# If version already exists or in dry-run, nothing else to do.
	[ -d "$Job/$item/.dim/$vers" -o "$Optn" ] && {
		[ "$Optn" ] && [ "$Optv" ] && echo "$arname" >&2
		pversion $item $vers
		return
	}

	# Obtain the list of exported anc2
	getexportedanc2 $item $lvers && lanc2=$R1

	# Update descendants and ancestors with new name.
	lget "$Job/$item/.dim/$lvers/anc" && anc=$R1
	ldel "$Job/$item/.dim/$anc/desc" $lvers
	ladd "$Job/$item/.dim/$xanc/desc" $vers
	echo $xanc >|$Job/$item/.dim/$lvers/anc
	lget "$Job/$item/.dim/$lvers/desc" && desc=$R1
	for d in $desc
	do
		echo "new anc of $d: $vers"
		echo $vers >|$Job/$item/.dim/$d/anc
	done

	# Update anc2 and corresponding desc2
	for v in $lanc2
	do
		ldel "$Job/$item/.dim/$v/desc2" $lvers
		ladd "$Job/$item/.dim/$v/desc2" $vers
	done
	lget "$Job/$item/.dim/$lvers/desc2" && ldesc2=$R1
	for d in $ldesc2
	do
		echo "new anc2 of $d: $vers"
		ldel "$Job/$item/.dim/$d/anc2" $lvers
		ladd "$Job/$item/.dim/$d/anc2" $vers
	done

	# Update version
	echo "$vers" >|$Job/$item/.sum/$sum
	[ -f "$Job/$item/.version/$vers" ] ||
	ln "$Job/$item/.dim/$lvers/sum" "$Job/$item/.version/$vers"

	# Rename local into global
	mv "$Job/$item/$item-$lvers" "$Job/$item/$item-$vers"
	mv "$Job/$item/.dim/$lvers" "$Job/$item/.dim/$vers"

	# Add previous local branch in anc2
	addanc2 $item $vers $anc

	# Update log
	echo ${elog:+"$elog"} >|$Job/$item/.dim/$vers/log

	# Sign metadata with author private key
	{
		date -u +'%Y-%m-%d %H:%M:%S UTC' |
		tee $Job/$item/.dim/$vers/date
		echo "$Email" | tee $Job/$item/.dim/$vers/from
		echo $sum
		echo $item-$vers
	} | sign >|$Job/$item/.dim/$vers/sign

	# Make an incremental archive
	[ "$Optv" ] && echo "$arname" >&2
	$arcmd $item $vers >$arname
	pversion $item $vers
}

sign() { openssl sha1 -hex -sign "$DIMRC"; }

sign2() { openssl rsautl -sign -inkey "$DIMRC" | openssl base64 | urlenc; }

# Usage: verify key sign
vsign()
{
	printf $(awk -v s=$2 'BEGIN {gsub(/../, "\\x&", s); print s}') >/tmp/bsign.$$
	openssl sha1 -verify "$1" -signature /tmp/bsign.$$
	rm /tmp/bsign.$$
}

# Usage: checksign item-vers
# Verify the authentication signature using the RSA public key of author.
checksign()
{
	local item vers p f date from sum res
	
	item=${1%%-*} vers=${1#*-}
	isexp $vers || return 2
	p=$Job/$item/.dim/$vers
	for f in sign from date sum
	do
		lget "$p/$f" && eval $f=\$R1 || return 2
	done
	res=$(printf "%s\n%s\n%s\n%s\n" "$date" $from $sum $item-$vers |
	      vsign ${Job%/*}/user/$from $sign)
	[ "$Optv" -gt 1 ] && echo "$item-$vers from $from, $date: $res" >&2
        case "$res" in
	(*\ OK)	res=0 ;;
	(*)	res=1 ;;
	esac
	return $res
}

cmd mkjob '[-nqv] Url [Dir]' 'Create a new job served by library Url' \
'	Create a new job served by the library Url in Dir. \
	The library acts as a repository and a version server.'
mkjob()
{
	local url=$1 job=${2:-.}
	case "$url" in
	(http:*|file:*|rsync:*)	;;
	('')	die "no url specified" ;;
	(*:*)	nousage=1 die "invalid protocol ${url%%:*}:
Supported protocols are http:, file: or rsync:" ;;
	(*)	nousage=1 die "invalid url $url
Supported protocols are http:, file: or rsync:" ;;
	esac
	getdir "$job" && {
		[ "$R2" ] && die "$job is a file"
		job=$R1
		setjob "$job" && [ "$job" != "$Job" ] &&
		nousage=1 die "${Job%/.dimlib/item} is already a job"
	}
	[ -d "$job" ] && {
		for j in "$job"/.dimlib "$job"/*/.dimlib "$job"/*/*/.dimlib
		do
			[ -d "$j" ] &&
			nousage=1 die "${j%/.dimlib} is already a job"
		done
		j=$(find "$job" -type d -name .dimlib -prune 2>/dev/null |
		    head -1)
		[ "$j" ] && nousage=1 die "$j is already a job"
	}
	[ "$Optv" ] && echo "$job served by $1" >&2
	[ "$Optn" ] && return
	job=$job/.dimlib
	mkdir -p "$job/archive" "$job/item" "$job/user" || die "mkjob failed"
	echo "$1" >$job/url
}

cmd mkitem '[-nqv] Item...' 'Create a new item Item'
mkitem()
{
	local item
	for item
	do
		# Authorize only alphanum chars for item name
		case "$item" in
		(*[!A-Za-z0-9_]*) die "invalid item name: $item"
		esac
		[ -f "$Job/$item/.version/0" ] && return
		[ "$Optn" ] && return
		# Create and populate item-0
		mkdir -p "$Job/$item/$item-0" "$Job/$item/.dim/0" \
			 "$Job/$item/.sum" "$Job/$item/.version" \
			 "$Job/$item/.removed" "$Archive/$item"
		>$Job/$item/.dim/0/index
		echo 0 >$Job/$item/.sum/$Zerosum
		echo $Zerosum >$Job/$item/.dim/0/sum
		ln "$Job/$item/.dim/0/sum" "$Job/$item/.version/0"
	done
}

getshortname()
{
	printf "Enter your short name or initials: "
	read R1
	case "$R1" in (*[!A-Za-z_]*) error "invalid entry"; return 1;; esac
}

cmd mkuser '[Arg]' 'Create a new user' \
'	Create a new user metadata record. If necessary, create
	~/.dimrc first.'
mkuser()
{
	local umask furl turl sname res uname umail ukey pubkey
	# Generate a private user configuration file
	[ -f "$DIMRC" ] || {
		printf "Enter your email address: "
		read Email
		[ "$Email" ] || die no email entered
		umask=$(umask)
		umask 077
		[ "$Optn" ] && trap "rm \"$DIMRC\"" EXIT
		cat <<- EOT >$DIMRC || die
		Email='$Email'
		: Rsa_key='
		$(openssl genrsa 2>|/dev/null)
		'
		EOT
		umask $umask
	}
	# Generate a public user file
	furl=${Url#file:} turl=${Url%%:*}
	if [ "$turl" = file -a ! -f "${Job%/*}/user/$Email" ]
	then
		[ -d "$furl/user" ] || mkdir -p "$furl/user"
		while true
		do
			getshortname && sname=$R1 || continue
			echo "$Email" >$furl/user/$sname && break
		done
		[ "$Optn" ] && {
			rm "$furl/user/$sname"
			echo "$sname <$Email>"
			return
		}
		cat <<- EOT >$furl/user/$Email || die
		sname='$sname'
		pubkey='
		$(openssl rsa -pubout <$DIMRC 2>|/dev/null)
		'
		EOT
		[ "$furl" != "${Job%/item}" ] &&
		cp "$furl/user/$Email" "${Job%/item}/user/"
		echo "$sname <$Email>"
	elif [ "$turl" = rsync -a ! -f "${Job%/*}/user/$Email" ] 
	then
		cd "${Job%/*}"
		while true
		do
			getshortname && sname=$R1 || continue
			echo "$Email" >user/$sname
			rsync --ignore-existing -R user/$sname \
			      ${Url#rsync:} || {
			      rm user/$sname
			      nousage=1 die "rsync failed"
			}
			rsync ${Url#rsync:}/user/$sname user/$sname || {
				rm user/$sname
				nousage=1 die
			}
			read res <user/$sname
			[ "$res" = "$Email" ] && break
		done
		cat <<- EOT >user/$Email || die
		sname='$sname'
		pubkey='
		$(openssl rsa -pubout <$DIMRC 2>|/dev/null)
		'
		EOT
		rsync --ignore-existing -R user/$Email "${Url#rsync:}/" ||
		nousage=1 die "rsync failed"
		echo "$sname <$Email>"
	elif [ "$turl" = http -a ! -f "${Job%/*}/user/$Email" ] 
	then
		[ "$Optn" ] && die "this function cannot be simulated"
		umail=$(printf %s "$Email" | urlenc)
		pubkey=$(openssl rsa -pubout <$DIMRC 2>|/dev/null)
		uname=$(printf %s "$name" | urlenc)
		ukey=$(printf %s "$pubkey" | urlenc)
		if [ ! "$1" ]
		then
			Http_data="name=$uname&mail=$umail&pkey=$ukey" \
			     wget "$Url?nu=2"
			return
		fi
		sign=$(printf %s "$Email" | sign2)
		while true
		do
			getshortname && sname=$R1 || continue
			res=$(Http_data="sn=$sname&key=$1&p=$sign" \
			      wget "$Url?nuc=1")
			case "$res" in
			(ok)		break ;; 
			(Error:*)	continue ;;
			(*)		die "$res";;
			esac
		done
		echo "$sname <$Email>"
	fi
}

# Usage: getitem path
# Output: R1=item R2=nclone R3=rootdir
# Return the item containing path
getitem()
{
	local d f cdir curdir=$PWD
	[ "$1" ] || set .
	getdir "$1" && d=$R1 f=$R2 || die "invalid dir $1"
	# If $d is under $Job, return the corresponding item
	case "$d" in ("$Job"/*)
		cdir=${d#$Job/}
		R1=${cdir%%/*} R2=
		return
	esac
	# return the first clone which links to the dir or one of its parents
	cd "$Job"
	while [ "$d" ]
	do
		for cdir in */.dim/*+*/dir
		do
			[ "$cdir" = "*/.dim/*+*/dir" ] && break 2
			[ "$Job/$cdir" -ef "$d" ] && {
				cd "$curdir"
				R1=${cdir%%/*}
				cdir=${cdir%/dir}
				R2=${cdir##*+} R3=$d
				return
			}
		done
		d=${d%/*}
	done
	cd "$curdir"; return 1
}

# Usage: fix_sha1 -|x 
# Transform output of "openssl sha1" into index. Avoid use of -r xargs flag.
fix_sha1()
{
	awk -v prefix="$1 " '{
		len = length()
		file = substr($0, 8, len - 50)
		sum = substr($0, len - 39)
		if (file) print prefix sum " " file
	}'
}

do_sum()
{
	find . -type f -perm -0100 $Fprint0 | xargs $X0 openssl sha1 |
		fix_sha1 x
	find . -type f ! -perm -0100 $Fprint0 | xargs $X0 openssl sha1 |
		fix_sha1 -
	[ "$Fempty" ] && find . -type d $Fempty ! -name . |
			 awk '{print "d '$Zerosum' " substr($0, 3)}'
}

# Usage: update_sum item numclone
# Compute sha1sum of current dir and store it in clone metadata
update_sum()
{
	local item=$1 nclone=$2 list f sum p nscan=$Job/$item/.dim/+$2/scan \
	      nsum=$Job/$item/.dim/+$2/sum nindex=$Job/$item/.dim/+$2/index \
	      rootdir
	lget "$Job/$item/.dim/+$nclone/path" && rootdir=$R1
	cd "$Job/$item/$item+$nclone"
	if [ -f "$nscan" -a -f "$nsum" ] 
	then			 # Incremental mode
		list=/tmp/nlist.$$
		find . -type f \( $Fcnewer "$nscan" -o -links 1 \) >|$list
		[ -s "$list" ] || { read R1 <$nsum; return; }
		>|$nscan
		while read -r f
		do
			# FIXME: encode filename
			[ "$f" -ef "$rootdir/$f" ] || {
				[ -f "$rootdir/$f" ] &&
				ln -f "$rootdir/$f" "$f" || {
					# remove file from index
					rm -f "$Job/$item/$item+$nclone/$f"
					echo "r $Zerosum ${f#./}"
				}
				[ -f "$f" ] || continue
			}
			sum=$(openssl sha1 <$f)
			sum=${sum#* }	# required for openssl-0.9.9
			[ -x "$f" ] && p=x || p=-
			echo "$p $sum ${f#./}"
		done <$list |
		awk '
		$1 == "r" {removed[substr($0, 44)] = 1; next}
		{changed[substr($0, 44)] = 1; print}
		END {
			while (getline <"'$nindex'" > 0) {
				fname = substr($0, 44)
				if (fname in removed) continue
				if (! (fname in changed)) print
			}
		}' |
		sort -k3 >|$nindex.$$
		mv $nindex.$$ $nindex
		rm $list
	else			# Absolute mode
		>|$nscan
		do_sum | sort -k3 >|$nindex
	fi
	R1=$(openssl sha1 <$nindex)
	R1=${R1#* }	# required for openssl-0.9.9
	echo $R1 >|$nsum
}

cmd add '[-nqv] Path...' 'Add Path to clone index' \
'	Add Path to clone index. Directories are added recursively.'
add()
{
	local path act d f script index_tree_root findopt finddir rp item \
	      nclone clonerootdir
	# normalize arguments: absolute
	for path
	do
		case "$path" in
		(/*)	set -- "$@" "$path" ;;
		(*)	set -- "$@" "$PWD/$path" ;;
		esac
		shift
	done
	for path
	do
		set --
		getdir "$path" && d=$R1 f=$R2
		getiv "$d" && item=$R1 nclone=${R2#+}
		isclone "$R2" ||  die "not in a clone: $d"
		index_tree_root=$Job/$item/$item+$nclone
		lget "$Job/$item/.dim/+$nclone/path" && clonerootdir=$R1
		if [ "$_Cmd" != del ]
		then
			t="rm -f \"$Job/$item/.dim/+$nclone/scan\"; cpio -pdul"
			script="$t \"$index_tree_root\" 2>/dev/null"
		fi
		case "$_Cmd" in
		(del)
			act=del findopt="( -depth ! -name . -print )"
			script="rm -f \"$Job/$item/.dim/+$nclone/scan\"; xargs rm -r" 
			finddir=$index_tree_root
			;;
		(*)
			act=add
			findopt="( -type f -print )"
			[ "$Fempty" ] && 
			findopt="$findopt -o ( -type d $Fempty ! -name . -print )"
			finddir=$clonerootdir
			# exclude mount points of suppliers
			Optp=1 supplier >|/tmp/supplier.$$
			while read d
			do
				# normalize supplier: relative to finddir
				lget "${d%+*}/.dim/+${d##*+}/path" && d=$R1
				relpath "$d" "$finddir" && rp=$R1 || rp=
				set -- "$@" \( -path "./${rp%/}" -prune \) -o
			done </tmp/supplier.$$
			rm /tmp/supplier.$$
			;;
		esac
		cd "$finddir" || die "cannot chdir $finddir"
		# normalize argument: relative to clonerootdir
		relpath "$path" "$clonerootdir" && rp=$R1 || rp=
		[ "$rp" ] && path=./${rp%/} || path=.
		[ -e "$path" ] ||
		nousage=1 die "$finddir/${path#./}: No such file or directory"
		find "$path" "$@" $findopt |
		awk -v Optn="$Optn" -v Optv=${#Optv} -v \
			act=$act -v cmd=$Cmd -v script="$script" '
		$0 ~ /^\.\// { $0 = substr($0, 3) }
		{
			if ((Optv > 1) ||
			   ((cmd == "add" || cmd == "del") && Optv > 0))
				printf act " %s\n", "'"$index_tree_root/"'"$0 >"/dev/stderr"
			if (! Optn) print $0 | script
		}'
	done
}

cmd del '[-nqv] Path' 'Remove Path from clone index'
del() { _Cmd=del add "$@"; }

cmd check '[-aqv] [-0-9] Vers' 'Check integrity of version Vers'
check()
{
	local i v item vers sum sres 
	[ "$1" ] || {
		[ "$Opta" ] && set -- $(item) || set -- $(version)
	}
	for i
	do
		for v in $(Opts= OptS= Optp= version "$i")
		do
			case "$v" in
			(*+*)	item=${v%%+*} vers=+${v#*+}
				update_sum $item ${vers#+} && sum=$R1
				;;
			(*)	item=${v%%-*} vers=${v#*-}
				checksign $item-$vers
				case $? in
				(0) sres='sign=ok' ;;
				(1) sres='sign=bad' ;;
				(*) sres= ;;
				esac
				;;
			esac
			vcheck $item $vers && cres=ok || cres=ok
			echo "$item-$vers sum=$cres $sres"
		done
		[ "$Opta" ] && break
	done
}

# Usage: vcheck item vers
# Return true if checksum and signature of itemvers is ok, false otherwise.
vcheck()
{
	local item=$1 refsum p pv isum odir sum
	lget "$Job/$item/.dim/$2/sum" && refsum=$R1
	Opts= OptS= Optp=1 getpvers $item $2 && p=$R1
	getpvers $item $1 && pv=$R1
	isum=$(openssl sha1 <$Job/$item/.dim/$2/index)
	isum=${isum#* }	# required for openssl-0.9.9
	[ "$isum" = "$refsum" ] || {
		error "inconsistent index: $pv"
		return 1
	}
	odir=$PWD sum=
	cd "$p" && sum=$(do_sum | sort -k3 | openssl sha1) || return 1
	sum=${sum#* }	# required for openssl-0.9.9
	[ "$sum" = "$refsum" ] || {
		error "corrupted version: $pv"
		do_sum | checkindex "$Job/$item/.dim/$2/index"
		cd $odir
		return 1
	}
	cd $odir
}

# arg1: reference index, stdin: real index
checkindex()
{
	awk -v f1="$1" '
	BEGIN {
		while (getline line <f1 > 0)
			c1[substr(line, 44)] = substr(line, 1, 42)
	}
	{ c2[substr($0, 44)] = substr($0, 1, 42) }
	END {
		for (f in c1)
			if (!(f in c2)) 		# deletion
				print " removed " f
			else if (c1[f] != c2[f]) { 	# change 
				split(c1[f], a1); split(c2[f], a2)
				if (a1[1] != a2[1])
					print (a1[1] == "x" ? " +x " : " -x ")f 
				if (a1[2] != a2[2])
					print " changed " f
			}
		for (f in c2)
			if (!(f in c1))
				print " added " f	# creation
	}
	'
}

cmd merge '[-nqv] Vers' 'Merge Vers into current clone' \
'	Merge version Vers into current clone.'
merge()
{
	local item nclone v2 sum v lvers p1 p2 canc p0 d0 d1 d2 mdp a2 file \
	      dir copt nindex curdir=$PWD
	[ $# = 1 ] || die "invalid number of arguments"
	getiv && item=$R1 nclone=${R2#+}
	isclone "$R2" || die "not in a clone"
	nindex=$Job/$item/.dim/+$nclone/index
	getiv "$1" && v2=$R2
	[ "$R1" = "$item" -a "$v2" ] || die "invalid version: $1"

	# Save the current clone content
	update_sum $item $nclone && sum=$R1
	cd "$curdir"
	lget "$Job/$item/.sum/$sum" && v=$R1 || { 
		lvers=$(Cmd=save Opts=1 Optb= OptM= save)
		[ "$Optn" ] && sum=$(do_sum | sort -k3 | openssl sha1) &&
		sum=${sum#* } || read sum <$Job/$item/.dim/$lvers/sum
	}
	getpvers $item +$nclone && p1=$R1
	getpvers $item $v2 && p2=$R1

	# Avoid merging twice the same content
	[ $v2 = +$nclone ] && nousage=1 die "cannot merge clone into itself"
	isanc2 $item $v2 +$nclone && {
		[ "$Optv" ] && echo "$p2 already merged" >&2
		return
	}

	# Proceed. Get common ancestor, print it
	getcanc $item +$nclone $v2 && canc=$R1
	getpvers $item $canc && p0=$R1
	[ "$Optv" ] && echo "$p0 is the common ancestor of $p1 and $p2" >&2
	Opts= OptS= Optp=1 getpvers $item $canc && d0=$R1
	Opts= OptS= Optp=1 getpvers $item $v2 && d2=$R1
	d1=$Job/$item/$item+$nclone mdp=$Job/$item/.dim

	# run the merge from index files
	awk -v f0="$mdp/$canc/index" -v f1="$nindex" -v f2="$mdp/$v2/index" '
	BEGIN {
	while (getline <f0 > 0) c0[substr($0, 44)] = substr($0, 1, 42)
	while (getline <f1 > 0) c1[substr($0, 44)] = substr($0, 1, 42)
	while (getline <f2 > 0) c2[substr($0, 44)] = substr($0, 1, 42)
	# compute diffs i0 -> i1 (current) and i0 -> i2
	for (f in c0) {
		if (! (f in c1)) del01[f] = 1
		else if (c0[f] != c1[f]) mod01[f] = 1
		if (! (f in c2)) del02[f] = 1
		else if (c0[f] != c2[f]) mod02[f] = 1
	}
	for (f in c1)
		if (! (f in c0)) {
			add01[f] = 1; g = f
			while (g ~ /\//) {
				sub("/[^/]*$", "", g)
				if (dir01[g]) break
				dir01[g] = 1
			}
		}
	for (f in c2)
		if (! (f in c0)) {
			add02[f] = 1; g = f
			while (g ~ /\//) {
				sub("/[^/]*$", "", g)
				if (dir02[g]) break
				dir02[g] = 1
			}
		}
	# Apply modifs i0 -> i2 to i1 (current) and resolve conflicts.
	# Modification takes precedence over deletion and stability.
	# Directory creation takes precedence over file creation.
	# Deletion takes precedence over stability.
	for (f in mod02)
		if (f in mod01) {
			if (c1[f] != c2[f]) {
				if (c0[f] == c1[f]) print "copy " f
				else if (c0[f] != c2 [f]) print "merge3 " f
			}
		} else if (! (f in dir01)) print "copy " f
	for (f in add01)
		if (f in dir02) print "remove " f
	for (f in add02)
		if (f in add01) {
			if (c1[f] != c2[f]) print "merge2 " f
		} else if (! (f in dir01)) print "copy " f
	for (f in del02)
		if (! (f in mod01)) print "remove " f
	}' |
	while read -r cmd file
	do
		echo "$cmd $file"
		[ "$Optn" ] && continue
		dir=${file%/*}
		[ "$file" = "$dir" -o -d "$dir" ] || mkdir -p "$dir" "$d1/$dir"
		case $cmd in
		(copy)
			cp "$d2/$file" "$file" && ln -f "$file" "$d1/$file" ;;
		(merge2)
			copt="--changed-group-format=\"<<<<<<< $d1/$file
%<=======
%>>>>>>>> $d2/$file
\""
			diffcmd ${DiffC:+$copt} "$d1/$file" "$d2/$file" >$file.merge2 
			[ -x "$file" ] && chmod +x "$file.merge2"
			# Use cp instead of mv to preserve hardlinks
			cp "$file.merge2" "$file"
			rm "$file.merge2"
			;;
		(merge3)
			# The awk filter removes diff3 false conflicts
			# identified by the following pattern:
			#
			# <<<<<<< $d0/$file
			# text to be discarded
			# =======
			# text to be preserved
			# >>>>>>> $d2/$file
			#
			diff3 -m "$d1/$file" "$d0/$file" "$d2/$file" |
			awk '/^<<<<<<< / && substr($0, 9) == "'$d0/$file'" {
				while (getline > 0)
					if ($0 == "=======") break
				while (getline > 0)
					if ($0 ~ /^>>>>>>> /) next
					else print
			}
			{print}' >$d1/$file.merge3
			[ -x "$d1/$file" ] && chmod +x "$d1/$file.merge3"
			# Use cp instead of mv to preserve hardlinks
			cp "$d1/$file.merge3" "$d1/$file"
			rm "$d1/$file.merge3"
			;;
		(remove)
			rm -f "$file" && rm -f "$d1/$file" ;;
		esac
	done
	[ "$Optn" ] && return

	# FIXME: removing of empty directories should be reexamined
	find . -depth -type d $Fprint0 | xargs $X0 rmdir -p 2>/dev/null

	# Keep the ancestor graph reduced: delete redundant vers anc2
	getanc2 $item +$nclone && a2=$R1
	for v in $a2
	do
		isanc2 $item $v $v2 && rmanc2 $item +$nclone $v
	done
	addanc2 $item +$nclone $v2
	rm -f "$Job/$item/.dim/+$nclone/scan"	# Force index refresh
}

cmd merged '[-nqv] [Vers]' 'Declare Vers merged in current clone'
merged()
{
	local item nclone v2 p2 a2 v
	getiv && item=$R1 nclone=${R2#+}
	isclone "$R2" || die "not in a clone"
	getiv "$1" && v2=$R2
	[ "$R1" = "$item" -a "$v2" ] || die "invalid version: $1"
	getpvers $item $v2 && p2=$R1
	# Avoid merging twice the same content
	[ $v2 = +$nclone ] &&
	nousage=1 die "cannot merge clone into itself"
	isanc2 $item $v2 +$nclone && {
		[ "$Optv" ] && echo "$p2 already merged" >&2
		return
	}
	# Keep the ancestor graph reduced: delete redundant vers anc2
	getanc2 $item +$nclone && a2=$R1
	for v in $a2
	do
		isanc2 $item $v $v2 && rmanc2 $item +$nclone $v
	done
	addanc2 $item +$nclone $v2
	rm -f "$Job/$item/.dim/+$nclone/scan"	# Force index refresh
}

cmd unmerge '[-afnqv] [Vers]' 'Undo a merge operation' \
'       Remove Vers from the ancestor2 list of current clone.
	With -f, restore modified files from ancestor.
	With -a, process all ancestor2 of current clone.'
unmerge()
{
	local item nclone a2 vers v pv
	getiv && item=$R1 nclone=${R2#+}
	isclone "$R2" || die "not in a clone"
	getanc2 $item +$nclone && a2=$R1 || die "nothing to unmerge"
	if [ "$1" ]
	then
		getiv "$1" && vers=$R2
		[ "$R1" = "$item" -a "$vers" ] || die "invalid version: $1"
		lin "$a2" $vers || die "invalid ancestor2: $vers"
	else 
		[ "$Opta" ] ||
		case "$a2" in (*\ *) die "cannot choose anc2" ;; esac
		vers=$a2
	fi
	for v in $vers
	do
		getpvers $item $v && pv=$R1
		[ "$Optv" ] && echo "removing anc2 $pv" >&2
		[ "$Optn" ] || rmanc2 $item +$nclone $v
	done
	[ "$Optf" ] && copy
}

cmd mount '[-nqv] [Item|Vers [Dir]]' \
'Mount a version on a dir, or list' \
'	Associate the item Item to the working directory Dir. If
	a version Vers is specified, change the ancestor to Vers
	instead of default 0. If no arguments, list existing mounts.'
mount()
{
	local lv item citem m i n a p o nitem nclone nnclone nrootdir nanc \
	      ndir nfile vers curdir
	# 0 or 1 args: list existing mounts
	[ $# -lt 2 ] && {
		getiv "$1" && item=$R1 lv=$R2
		lv=${lv:-0}
		for m in "$Job"/*/.dim/+*
		do
			[ "$m" = "$Job/*/.dim/+*" ] && continue
			i=${m#$Job/}; i=${i%/.dim/*}	# item name
			n=${m##*/}			# clone name
			lget "$m/anc" && a=$R1  || a=	# clone ancestor
			lget "$m/path" && p=$R1 || p=	# clone path
			[ "$i-$a" = "$item-$lv" -o "$i$n" = "$item$lv" -o \
			  "$item-$lv" = "$i-0" -o ! "$1" ] &&
			echo "$i-$a on $p ($i$n) $o"
		done
		return
	}

	# Various checks
	[ $# = 2 ] || {
		error "invalid number of arguments"
		return 1
	}
	cd "$2" && curdir=$PWD || {
		error "invalid directory: $2"
		return 1
	}
	getiv && citem=$R1 nclone=${R2#+}
	isclone "$R2" || nclone=

	getitem "$curdir" && nitem=$R1 nnclone=$R2 nrootdir=R3
	[ "$nclone" ] && {
		getanc $citem +$nnclone && nanc=$R1
		m="$nitem-$nanc is already mounted on $nrootdir ($nitem+$nclone)"
		[ "$curdir" -ef "$nrootdir" ] && nousage=1 die "$m"
		[ ${1%%-*} == $nitem ] && nousage=1 die "$m"
		relpath "$curdir" "$nrootdir" && ndir=$R1
		ndir=${ndir%/}
	}

	# Check that current dir is not under $Job
	case "$curdir" in
	("$Job"|"$Job"/*)
		error "directory \"$curdir\" under $Job"
		return 1 ;;
	esac

	# Check item and version
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || {
		error "invalid version: $1"
		return 1
	}

	# Create a new clone
	nclone=1
	while [ -d "$Job/$item/.dim/+$nclone" ]
	do
		nclone=$(($nclone + 1))
	done
	[ "$Optv" ] && echo "$item-$vers on $curdir ($item+$nclone)" >&2
	[ "$Optn" ] || {
		mkdir -p "$Job/$item/.dim/+$nclone" "$Job/$item/$item+$nclone"
		echo $vers >$Job/$item/.dim/+$nclone/anc
		ladd "$Job/$item/.dim/$vers/desc" +$nclone
		echo "$curdir" >$Job/$item/.dim/+$nclone/path
		ln -s "$curdir" "$Job/$item/.dim/+$nclone/dir"
	}
	[ "$ndir" ] && {
		if [ -e $Job/$nitem/$nitem+$nnclone/$ndir ]
		then
			del "$nitem+$nnclone:$ndir"
		fi
		[ "$Optn" ] || rm -f "$Job/$nitem/.dim/+$nnclone/scan"
	}
	cd "$curdir"
	pversion $item +$nclone
}

cmd remount '[-nqv] [Vers [Dir]]' 'Change mount parameters' \
'	Change the ancestor of the current working directory to
	Vers. A version must already be mounted to the working
	directory. Change of Item or mount point is not permitted.'
remount()
{
	local item nclone vers u m o oanc curdir=$PWD
	getiv "$1" && item=$R1 vers=$R2 
	[ "$Cmd" != remount ] && vers=$1 || {
		[ "$vers" ] || die "invalid version: ${1:-.}"
		[ "$1" ] || { Narg=1 getanc $item $vers && vers=$R1; }
	}
	isclone "$vers" && die "cannot remount to $item$vers"

	getiv $curdir && nclone=${R2#+}
	isclone "$R2" || die "not in a clone: $curdir"

	[ "$Cmd" = remount ] && getitem "$curdir" && u=$R1 nclone=$R2
	[ "$nclone" ] || die "not a clone: $curdir"

	# Update clone metadata
	lget "$Job/$item/.dim/+$nclone/anc" && oanc=$R1 || oanc=0
	[ "$Optv" ] &&
	echo "remount $item-$vers on $curdir ($item+$nclone)" >&2
	[ "$Optn" ] && return
	ldel "$Job/$item/.dim/$oanc/desc" +$nclone
	echo $vers >|$Job/$item/.dim/+$nclone/anc
	ladd "$Job/$item/.dim/$vers/desc" +$nclone
	[ -f "$Job/$item/.dim/$vers/scan" ] &&
	rm -f "$Job/$item/.dim/$vers/scan"
	addanc2 $item +$nclone $oanc
}

cmd copy '[-fnqv]' 'Copy files from ancestor to clone'
copy()
{
	local item nclone anc sum vers report oindex nindex curdir=$PWD
	getiv $curdir && item=$R1 nclone=${R2#+}
	isclone "$R2" || die "not in a clone: $curdir"
	getanc $item +$nclone && anc=$R1

	# Check that content is saved
	update_sum $item $nclone && sum=$R1
	lget "$Job/$item/.sum/$sum" && vers=$R1 || { 
		[ "$Optf" ] && report=warn || report=die
		nousage=1 $report "directory content not saved"
	}
	[ "$vers" = "$anc" ] && return

	# Apply delta between ancestor and clone
	[ "$Optv" -gt 1 ] && echo "delta from $item-$anc to $item+$nclone" >&2
	oindex=$Job/$item/.dim/+$nclone/index
	nindex=$Job/$item/.dim/$anc/index
	# copy from ancestor to clone working dir
	delta "$oindex" "$nindex" "$Job/$item/$item-$anc" "$curdir"
	[ "$Optn" ] && return
	# link from clone working dir to clone cache
	Cpio_link=l Optv= delta "$oindex" "$nindex" "$curdir" \
				 "$Job/$item/$item+$nclone"
	cp "$nindex" "$oindex"
	>|$Job/$item/.dim/+$nclone/scan
}

cmd mkclone '[-nqv] Vers Dir' 'Create a new clone' \
'	Create a new clone Dir, mount the item version Vers
	on it, and populate it with the corresponding files.'
mkclone()
{
	local item vers odir=$PWD
	[ $# = 2 ] || die "invalid number of arguments"
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version: $1"
	[ -d "$2" ] && die "directory already exists: $2"
	mkdir "$2" || nousage=1 die
	mount $item-$vers "$odir/$2" || {
		cd "$odir" && rmdir "$2"
		return 1
	}
	if [ "$Optn" ]
	then
		cd "$odir" && rmdir "$2"
	else
		copy
	fi
}

cmd comment '[Dir]' 'Edit log of the current clone Dir' \
'	Edit the log file of the clone corresponding to Dir or
	current directory if empty.'
comment()
{
	local item nclone nlog
	getiv "$1" && item=$R1
	isclone "$R2" && nclone=${R2#+} || die "not in a clone"
	nlog=$Job/$item/.dim/+$nclone/log
	if [ -t 0 ]
	then
		${EDITOR:-vi} "$nlog"
	else
		cat >|$nlog
	fi
}

# Usage: isexp vers
# Return 0 if vers is an exported version
isexp() { case $1 in (*--*|*+*) return 1;; (*) return 0;; esac; }

# Usage: isclone vers
# Return 0 if vers is a clone form (1st char is '+')
isclone() { test "${1%%[!+]*}"; }

# Usage: ismain vers
# Return 0 if vers is a main version
ismain() { case $1 in (*[-+]*) return 1;; (*) return 0;; esac; }

# Usage: isanc item v1 v2
# Return 0 if v1 is a direct or indirect ancestor of v2
isanc()
{
	R1=$3
	while [ "$R1" != 0 ]
	do
		getanc $1 $R1
		[ "$R1" = "$2" ] && return
	done
	return 1
}

# Usage: isanc2 item v1 v2
# Return 0 if v1 is a direct or indirect ancestor1 or 2 of v2
isanc2() { getclosure desc $1 $2 $3; }

# Usage: getclosure (anc|desc) item v1 [v2]
# Output: R1=list
# If v2 is provided, return 0 if v2 is (anc|desc) of v1
getclosure()
{
	local more=$3 new list res next i j
	while [ "$more" ]
	do
		new=$more more=
		for i in $new
		do
			lget "$Job/$2/.dim/$i/$1" && list=$R1 || list=
			lget "$Job/$2/.dim/$i/${1}2" && list="$list $R1"
			[ "$4" ] && lin "$list" $4 && return 0
			for j in $list
			do
				lin "$res" $j || res="$res $j" more="$more $j"
			done
		done
	done
	[ "$4" ] && return 1
	R1=$res
}

# Usage: getcanc item v1 v2
# Output: R1=canc
# Find the closest common ancestor of 2 versions, best candidate for merge3
getcanc()
{
	local item=$1 a1=$2 a2=$3 b1=$2 b2=$3 res v aa1 aa2 i ad
	# Direct case
	isanc2 $item $2 $3 && { R1=$2; return; }
	isanc2 $item $3 $2 && { R1=$3; return; }

	# Step1: quick, find a valid common ancestor, maybe not the best one
	while [ "$a1" != 0 -o "$a2" != 0 ]
	do			# walk from leaves back to root
		for v in $b1	# intercept the 2 branches
		do
			case " $b2 " in (*" $v "*) res=$v; break 2;; esac
		done
		if [ "$a1" != 0 ]
		then
			getanc $item $a1 && a1=$R1
			getanc2 $item $a1 && aa1=$R1
			b1="$b1 $aa1 $a1"
		fi
		if [ "$a2" != 0 ]
		then
			getanc $item $a2 && a2=$R1
			getanc2 $item $a2 && aa2=$R1
			b2="$b2 $aa2 $a2"
		fi
	done
	res=${res:-0}

	# Step2: slow, find the best candidate in all its descendants
	getclosure desc $item $res && ad=$R1
	for i in $ad
	do
		isanc2 $item $i $1 && isanc2 $item $i $2 &&
		isanc2 $item $res $i && res=$i
	done
	R1=$res
}

# Usage: getexportedanc2 item vers
# Output: R1=list_of_anc2
# Return the list of closest exported anc2 on separate branches.
getexportedanc2()
{
	local item=$1 vers=$2 xanc v vv la2 nla2 ad
	Opte=1 Opta= getanc $item $vers && xanc=$R1 	# exported anc
	getclosure desc $item $xanc && ad=$R1
	for v in $ad
	do
		isanc $item $v $vers && continue
		isanc2 $item $v $vers || continue
		nla2=
		for vv in $la2
		do
			isanc $item $vv $v || nla2="$nla2 $vv"
		done
		la2="$nla2 $v"
	done
	R1=$la2
}

# Usage: getlocalanc item vers
# Output: R1=list_of_local_versions_from_last_exported
getlocalanc()
{
	local item=$1 v=$2 anc ad la i
	Opte=1 Opta= getanc $item $v && anc=$R1
	getclosure desc $item $anc && ad=$R1
	for i in $ad
	do
		isexp $i && continue
		isanc2 $item $i $v || continue
		la="$la $i"
	done
	R1="$la $v"
}

# Usage: plog item vers
# Print a version log entry
plog()
{
	local IFS= log=$Job/$1/.dim/$2/log line pref="  "
	[ "$Cmd" = export ] || { pref=; pversion $1 $2; }
	[ -s "$log" ] &&
	while read -r line
	do
		[ "$line" ] && echo $pref$line
	done <$log
}

# Usage: setv1v2 [v1 [v2]]
# Output: R1=item R2=oldvers R3=newvers
setv1v2()
{
	local item anchor_vers v1 v2
	if [ "$2" ]
	then
		getiv "$2" && item=$R1 v2=$R2
		[ "$v2" ] || die "invalid version: $2"
		getiv "$1" && v1=$R2
		[ "$R1" = "$item" -a "$v1" ] || die "invalid version: $1"
		[ "$N1" ] && die "too many -<n> flags"
	else
		[ "$1" -a "$N2" ] && die "too many -<n> flags"
		getiv "$1" && item=$R1 anchor_vers=$R2
		[ "$anchor_vers" ] || die "invalid version: ${1:-.}"
		Narg=${N2:-0} Opta= getanc $item $anchor_vers && v2=$R1 ||
		die "invalid new"
		Narg=${N1:-1} Opta= getanc $item $anchor_vers && v1=$R1 ||
		die "invalid old"
	fi
	R1=$item R2=$v1 R3=$v2
}

cmd log '[-ae] [-0-9] [Vers [Vers2]]' 'Print log between old and new'
log()
{
	local d d1 lanc a v item v1 v2
	setv1v2 "$@" && item=$R1 v1=$R2 v2=$R3

	if [ "$Opte" ]
	then
		getiv "$1" && item=$R1 a=$R2
		getlocalanc $item $a && lanc=$R1
		for v in $lanc
		do
			plog $item $v
		done
		return
	fi

	# Compute the range segment: default is from old to new included
	# -e: old is the last exported ancestor (excluded)
	[ ! "$Opte" ] && d1=$(echo $v1; Opts=1 Opta=1 desc $v1) ||
	d1=$(Opts=1 Opta=1 desc $(Opte=1 anc $v2)) Opte=
	Opta=1 getanc $item $v2 && lanc=$R1
	# Print log entries from ancestor to descendants
	for d in $d1
	do
		case "$lanc" in			# skip not v2 ancestors
		("$d\n"*|*"\n$d\n"*|*"\n$d"|$d) continue;;
		esac
		[ $d = $v2 ] && continue	# skip v2
		plog $item $d
	done
	plog $item $v2
}

# Usage: relpath Path Dir
# Output: R1=Rpath
relpath()
{
	local rdir rfile
	getdir "$1" && rdir=$R1 rfile=$R2 || die "chdir error"
	[ "${rdir#$2}" = "$rdir" ] && rdir= || rdir=${rdir#$2}
	R1=${rdir:+${rdir#/}/}$rfile
}

cmd diff '[-cepSs] [-i File] [-o Optstr] [Vers [Vers2]]' 'Print differences' \
'	Print differences between ancestor Vers and descendant
	Vers2. The command wdiff is the prefered way to browse
	differences between two versions.'
diff()
{
	local item fp arg f1 f2 sum1 sum2 d1 d2 cd2 dr dp v1 v2 curdir=$PWD
	setv1v2 "$@" && item=$R1 v1=$R2 v2=$R3
	# Get the list of individual files to set filtering
	[ "$Iarg" ] && fp=$Iarg ||
	for arg
	do
		case "$arg" in (.|..|*/*) fp="$fp $arg"; esac
	done
	
	# Get checksum files or recompute checksums of current clones
	f1=$Job/$item/.dim/$v1/index
	isclone "$v1" && { update_sum $item ${v1#+} && sum1=$R1; } ||
	lget "$Job/$item/.dim/$v1/sum" && sum1=$R1
	[ "$sum1" ] || die "invalid version $v1"
	f2=$Job/$item/.dim/$v2/index
	isclone "$v2" && { update_sum $item ${v2#+} && sum2=$R1; } ||
	lget "$Job/$item/.dim/$v2/sum" && sum2=$R1
	[ "$sum2" ] || die "invalid version $v2"

	# And now, the result is ...
	[ "$ddiff" ] || {
		printf "old "; pversion $item $v1
		printf "new "; pversion $item $v2
	}
	[ "$sum1" = "$sum2" ] && return	# identical

	# Not identical, compute the delta from the indices
	Opts= OptS= Optp=1 getpvers $item $v1 && d1=$R1	# data dir of v1
	Opts= OptS= Optp=1 getpvers $item $v2 && d2=$R1	# data dir of v2

	lget "$Job/$item/.dim/$v2/path" && cd2=$R1 || cd2=$d2
	# Set additional filtering of PWD if in a subdir of v2
	case "$curdir" in
	("$cd2"/*) dr=${curdir#$cd2/}/; dp=$d2/$dr ;;
	(*) dp=$cd2 ;;
	esac
	# Get the list of individual files to set filtering
	# Delta of indices
	awk -v f1="$f1" -v f2="$f2" '
	BEGIN {
		while (getline line <f1 > 0)
			c1[substr(line, 44)] = substr(line, 1, 42)
		while (getline line <f2 > 0)
			c2[substr(line, 44)] = substr(line, 1, 42)
		for (f in c1)
			if (! (f in c2)) print "removed " f
			else if (c1[f] != c2[f]) print "changed " f
		for (f in c2)
			if (! (f in c1)) print "created " f
	}' | 
	# Filter individual files and/or subdirs
	while read -r act file
	do
		if [ "$Opta" ]
		then
			echo "$act $file"
		else
			for F in ${fp:-.}
			do
				relpath "${dp:+$dp/}${F#$dp}" "$d2" && G=$R1
				G=${G%/}
				[ ! "$G" ] && echo "$act $file" ||
				case $file in
				("$G"|"$G"/*) echo "$act $file";;
				esac
			done
		fi
	done | sort -k2 |
	if [ "$wdiff" ]	# Print word differences, file per file
	then
		[ "$OptN" -a "$OptO" ] && nocom=1 OptN= OptO=
		while read -r act file
		do
			[ -f "$d1/$file" ] && f1=$d1/$file || f1=/dev/null
			[ -f "$d2/$file" ] && f2=$d2/$file || f2=/dev/null
			istextfile "$f2" || continue
			wd ${OptN:+-1} ${OptO:+-2} ${nocom:+-3} -v "$f1" "$f2"
			echo
		done |
		if [ -t 1 ]	# output is an interactive terminal
		then
			less -CimnqGrX -j9 -h0 +/'\[0'
			printf '[00m' # force color reset
		else
			cat
		fi
	elif [ "$ddiff" -o "$Optstr" ]
	then
		while read -r act file
		do
			[ -f "$d1/$file" ] && f1=$d1/$file || f1=/dev/null
			[ -f "$d2/$file" ] && f2=$d2/$file || f2=/dev/null
			istextfile "$f2" || continue
			Opts= Optp= OptS= getpvers $item $v1 && iv1=$R1
			Opts= Optp= OptS= getpvers $item $v2 && iv2=$R1
			echo "diff $Optstr $iv1/$file $iv2/$file"
			diffcmd ${DiffL:+-L} "$iv1/$file" \
				${DiffL:+-L} "$iv2/$file" \
				$Optstr "$f1" "$f2"
			echo
		done
	elif [ "$Optc" ]	# scan unresolved merge conflicts
	then
		while read -r act file
		do
			[ -f "$d2/$file" ] && awk -v pref="${Optp:+$d2/}" '
			/^<<<<<<< / {n++}
			END {
				if (n) print n " unresolved conflicts in: "\
					     pref FILENAME
			}' $file 
		done
	elif [ "$Optp" ]	# Print files absolute pathnames
	then
		awk '{$2 = "'$d2/'" $2; print}'
	else			# Print files relative pathnames (default)
		while read -r act file
		do
			# Remove the part between version root and PWD
			# to get a real relative pathname.
			echo "$act ${file#$dr}"
		done
	fi
}

cmd ldiff '[-epSs] [-I File] [Vers [Vers2]]' 'Print differences'
ldiff() { ddiff=1 diff "$@"; }

cmd wdiff '[-epNOSs] [-I File] [Vers [Vers2]]' 'Print differences'
wdiff() { wdiff=1 diff "$@"; }

# Usage: delta old_index new_index src_dir dest_dir
# From delta between old and new, apply cp, ln or rm commands from src to dest
delta()
{
	cd "$3"
	awk -v f1="$1" -v f2="$2" -v ddir="$4" \
	    -v Optv="$Optv" -v Optn="$Optn" -v cpio="cpio -pdu$Cpio_link " '
	BEGIN {
		cp_cmd = cpio ddir " 2>/dev/null"
		rm_cmd = "cd " ddir " && xargs rm"
		removed = 0
		while (getline line <f1 > 0)
			c1[substr(line, 44)] = substr(line, 1, 42)
		while (getline line <f2 > 0)
			c2[substr(line, 44)] = substr(line, 1, 42)
		# Process remove first
		for (f in c1)
			if (! (f in c2)) {		# deletion
				if (Optv > 1) print "remove " f > "/dev/stderr"
				if (! Optn) print f | rm_cmd
				removed = 1
			}
		close(rm_cmd)
		# Remove empty directories (stupid brute force)
		# FIXME: be elegant and support empty dirs
		if (! Optn && removed)
			system("cd " ddir " && find . -depth -type d '$Fprint0' | 2>/dev/null xargs '$X0' rmdir -p")
		# Then apply changes or creations
		for (f in c1) 
			if (f in c2 && c1[f] != c2[f])  {	# change 
				if (Optv > 1) print "copy " f > "/dev/stderr"
				if (! Optn) print f | cp_cmd
			}
		for (f in c2) 
			if (! (f in c1)) { 		# creation
				if (Optv > 1) print "copy " f > "/dev/stderr"
				if (! Optn) print f | cp_cmd
			}
	}
	'
}

# Usage: lnd src dest index
# Duplicate src directory tree to dest, hard linking all files.
# Side effect: the current directory is changed to src
lnd()
{
	[ "$Optv" -gt 1 ] && echo "linktree ${1##*/} ${2##*/}" >&2
	[ "$Optn" ] && return
	[ -d "$2" ] || mkdir -p "$2" || die "invalid dir $2"
	cd "$1" &&
	awk '{print substr($0, 44)}' "$3" | cpio -pdul "$2" 2>/dev/null
}

# Usage: getnewvers item anc
# Output: R1=vers
# Generate a version name, given ancestor, branch flag and siblings
# The new version name is generated only in the local namespace,
# change at the right of '--'.
getnewvers()
{
	local item=$1 xanc=${2%--*} loc=${2#*--} v b lloc nloc
	[ "$OptM" -a "$Cmd" = save ] && OptM=
	if [ "$Optb" ]
	then	# Force a new branch
		if [ "$Branch" ]
		then
			case $Branch in
			(*[!-_.A-Za-z0-9]*|*--*|[-.]*)
				 die "invalid branch: $Branch" ;;
			(*[0-9]) ;;
			(*)	 Branch=${Branch}1 ;;
			esac
			if [ "$OptM" ]
			then
				v=$Branch
			else
				if [ "$Cmd" = export ]
				then
					case $Branch in
					(*[0-9]) ;;
					(*) Branch=${Branch}1 ;;
					esac
					v=$xanc-$Branch
				else
					[ "$loc" = "$2" ] &&
					v=$xanc--$Branch ||
					v=$xanc--$loc-$Branch
				fi
			fi
		else
			if [ "$Cmd" = export ]
			then
				v=$xanc.1
			else
				[ "$loc" = "$2" ] && loc=0
				v=$xanc--$loc.1
			fi
		fi
	else	# Do not force a new branch
		if [ "$OptM" ]	# Increment main ancestor last number
		then
			b=${xanc##*[!0-9]}
			v=${xanc%$b}$(($b + 1))
		elif [ "$loc" = "$2" ]
		then
			if [ "$Cmd" = export ]
			then
				case $xanc in
				(*-$sname*[!0-9.]*)	# not our branch, fork
					v=$xanc-${sname}1
					;;
				(*)			# keep on our branch
					b=${xanc##*[!0-9]}
					v=${xanc%$b}$(($b + 1))
					;;
				esac
			else
				v=$xanc--1
			fi
		else
			# Direct ancestor is a local version,
			# just increment the version number.
			lloc=${loc%.*}
			[ "$lloc" = "$loc" ] && nloc=$(($loc + 1)) ||
			nloc=$lloc.$((${loc##*.} + 1))
			v=$xanc--$nloc
		fi
	fi

	# Automatic branch creation if name collision
	while [ -d "$Job/$item/.dim/$v" ]
	do
		b=${v##*[!0-9]}		# get last digits
		v=${v%$b}$(($b - 1)).1	# fork a branch
	done
	R1=$v
}

cmd save '[-nvqb] [-B Vers] [Vers]' 'Save clone Vers to a new local version' \
'	Save the content of clone Vers or current into a readonly
	version of item. A unique local version name is returned.'
save()
{
	local item nclone ivers sum anc vers index ancsum csum overs lanc2 \
	      v nlog nindex curdir=$PWD rootdir
	getiv && item=$R1 nclone=${R2#+}
	isclone "$R2" || die "not in a clone: $PWD"
	nlog=$Job/$item/.dim/+$nclone/log
	nindex=$Job/$item/.dim/+$nclone/index
	update_sum $item $nclone && sum=$R1
	lget "$Job/$item/.sum/$sum" && ivers=$R1 && {
		pversion $item $ivers
		return
	}

	# Obtain a new version name
	lget "$Job/$item/.dim/+$nclone/anc" && anc=$R1
	getnewvers $item $anc && vers=$R1
	[ "$Optn" ] || mkdir "$Job/$item/.dim/$vers"

	# Read and duplicate ancestor (meta)data
	index=$Job/$item/.dim/$vers/index
	sum=$Job/$item/.dim/$vers/sum
	lget "$Job/$item/.dim/$anc/sum" && ancsum=$r1
	[ "$Optn" ] || cp "$Job/$item/.dim/$anc/index" "$index"
	lnd "$Job/$item/$item-$anc" "$Job/$item/$item-$vers" \
	    "$Job/$item/.dim/$anc/index"

	# Apply incremental delta between ancestor and clone to new
	lget "$Job/$item/.dim/+$nclone/path" && rootdir=$R1
	[ "$Optv" ] && echo "delta from $item+$nclone to $item-$vers" >&2
	delta "$Job/$item/.dim/$anc/index" "$nindex" "$rootdir" \
	      "$Job/$item/$item-$vers"
	[ "$Optn" ] && {
		Optv=1 remount $vers
		pversion $item $vers
		return
	}

	# Recompute new version indices at end of copy
	cd "$Job/$item/$item-$vers"
	do_sum | sort -k3 >|$index
	csum=$(openssl sha1 <$index)
	csum=${csum#* }	# required for openssl-0.9.9
	echo $csum >|$sum
	sum=$csum
	cd "$rootdir"

	# Check that final sum is really new, or revert.
	echo $vers >$Job/$item/.sum/$sum 2>/dev/null || {
		lget "$Job/$item/.sum/$sum" && overs=$R1
		remove $vers
		pversion $item $overs
	}

	# Update log and global metadata
	[ -s "$nlog" ] && cp "$nlog" "$Job/$item/.dim/$vers/log"
	>|$nlog
	echo $anc >|$Job/$item/.dim/$vers/anc
	ladd "$Job/$item/.dim/$anc/desc" $vers

	# Rewire the clone: new version becomes its ancestor
	Optv=1 remount $vers

	# Update anc2 and corresponding desc2
	lget "$Job/$item/.dim/+$nclone/anc2" && lanc2=$R1
	for v in $lanc2
	do
		ldel "$Job/$item/.dim/$v/desc2" +$nclone
		ladd "$Job/$item/.dim/$v/desc2" $vers
	done
	[ -f "$Job/$item/.dim/+$nclone/anc2" ] &&
	mv "$Job/$item/.dim/+$nclone/anc2" "$Job/$item/.dim/$vers/anc2"
	pversion $item $vers
}

# Usage: put_dir Url dir item
# dir must be a directory relative to the dim library rootdir
put_dir()
{
	local dir umail furl ra item=$3
	[ "$2" ] && dir=${Job%/*}/$2
	[ -d "$dir" ] || die "invalid directory $dir"
	
	case "$1" in
	(http:/*)
		umail=$(printf %s "$Email" | urlenc)
		;;
	(file:/*)		# Lazy creation of directory
		furl=${Url#file:}
		[ -d "$furl/$2" ] || {
			mkdir -p "$furl/$2" || die
			[ "$Optn" ] && trap "rmdir -p $furl/$2" EXIT
		}
		;;
	(rsync:*)
		cd ${Job%/*}
		rsync -Rdr --ignore-existing ${Optn:+-n} ${Optv:+-v} \
		      "$2" "${Url#rsync:}"
		return
		;;
	esac

	# Get list of archives on remote version server
	ra=$(get_filelist "$1" "$2")
	[ "$ra" = @error ] && die
	[ "$ra" = "*" ] && ra=
	cd "${Job%/*}/$2" || die

	# Send missing remote files
	{
		[ "$ra" ] && echo $ra
		echo; echo *; 
	} |
	awk 'BEGIN {init = 0}
	{
		if (init == 0) {
			if (NF == 0) {init = 1; next}
			gsub(/:[0-9]*/, "")
			for (i = 1; i <= NF; i++) remote[$i] = 1
		} else {
			for (i = 1; i <= NF; i++) {
				if ($i == "*") continue
				if (! ($i in remote)) print $i
			}
		}
	}' |
	while read f
	do
		size=$((0 + $(wc -c <$f)))
		[ "$Optv" ] && echo "put $f ($size bytes)" >&2
		[ "$Optn" ] && continue
		case "$Url" in
		(http:/*)
			# Handle max size upload limit: push 1M chunks
			if [ $size -gt 1048576 ]
			then
				odir=$PWD
				mkdir /tmp/bigurl.$$
				trap "rm -rf /tmp/bigurl.$$" EXIT
				cd /tmp/bigurl.$$
				split -b 1048576 <${Job%/*}/$2/$f
				for ff in *
				do
					size=$((0 + $(wc -c <$ff)))
					sign=$(printf %s $ff | sign2)
					Http_file="$ff" wget "$Url?upfile=1&item=$item&dfile=$ff&af=$f&m=$umail&p=$sign&s=$size"
				done
				cd "$odir"
			else
				sign=$(printf %s $f | sign2)
				Http_file="$f" wget "$Url?upfile=1&item=$item&dfile=$f&af=.&m=$umail&p=$sign&s=$size"
			fi
			;;
		(file:/*)
			[ -d "${Url#file:}/$2" ] ||
			mkdir -p "${Url#file:}/$2"
			cp "${Job%/*}/$2/$f" "${Url#file:}/$2/$f" 
			;;
		esac
	done
}

# Usage: get_dir url dir
get_dir()
{
	local rd f file size gsize curdir=$PWD
	cd "${Job%/*}/$2"  || die "invalid directory: $2"
	case "$1" in
	(rsync:*)
		cd ..
		rsync -rd ${Optn:+-n} ${Optv:+-v} ${1#rsync:}/$2 .
		return ;;
	esac
	rd=$(get_filelist "$1" "$2")
	[ "$rd" = @error ] && nousage=1 die "get_filelist failed"
	[ "$rd" = "*" ] && rd=
	for f in $rd
	do
		file=${f%:*} size=${f#*:}
		[ "$size" = "$f" ] && size=
		[ -f "$file" ] && {
			gsize=$((0 + $(wc -c <$file)))
			[ ! "$size" -o "$gsize" = "$size" ] && continue
		}
		[ "$Optv" ] &&
		echo "get $2/$file (${size:-?} bytes)" >&2
		[ "$Optn" ] && continue
		case "$Url" in
		(http:/*)
			wget "${1%/*}/$2/$file" >|$file || {
				rm "$file"
				continue
			} ;;
		(file:/*)
			cp "${1#file:}/$2/$file" "$file" || {
				rm "$file"
				continue
			} ;;
		esac
		gsize=$((0 + $(wc -c <$file)))
		[ ! "$size" -o "$gsize" = "$size" ] || {
			error "wrong size of $file: $gsize"
			rm "$file"
		}
	done
	cd "$curdir"
}

# Usage: get_filelist url dir
get_filelist()
{
	case "$1" in
	(http:/*) wget "$1?ld=$2" || echo @error ;;
	(file:/*) cd "${1#file:}/$2" && echo * || echo @error ;;
	(rsync:/*) rsync --list-only "${1#rsync:}/$2/" |
		   awk '$5 != "." {print $5 ":" $2}' || echo @error ;;
	esac
}

cmd get '[-anqv] [Item ...]' 'Receive new archives from mirror'
get()
{
	local user itemlist item i
	getiv && item=$R1
	[ "$Optv" ] && echo "local: ${Job%/*}
remote: $Url" >&2

	case "$Url" in
	(rsync:*)
		cd ${Job%/*}
		rsync -rd --exclude="*/item/[a-zA-Z]*" ${Optn:+-n} \
		      ${Optv:+-v} "${Url#rsync:}/" .
		return
		;;
	esac

	get_dir "$Url" user
	if [ "$Opta" -o ! "$item" ]
	then
		itemlist=$(get_filelist "$Url" item)
		[ "$itemlist" = @error ] && die
		[ "$itemlist" = "*" ] && itemlist=
		set -- $itemlist
	else
		[ $# = 0 ] && set -- $item
	fi
	for i
	do
		[ -d $Archive/$i ] || {
			mkdir -p $Archive/$i || die
			[ "$Optn" ] && trap "rmdir \"$Archive/$i\"" EXIT
		}
		get_dir "$Url" "archive/$i"
	done
}

cmd put '[-anqv] [Item]' 'Send new archives to mirror'
put()
{
	local itemlist item i
	getiv "$1" && item=$R1
	[ "$Optv" ] && echo "local: ${Job%/*}
remote: $Url" >&2

	[ $# -gt 0 -o \( ! "$Opta" -a "$item" \) ] && {
		put_dir "$Url" "archive/${1:-$item}" ${1:-$item}
		return
	}
	itemlist=$(Optp= Opta=1 item)
	for i in $itemlist
	do
		put_dir "$Url" archive/$i $i
	done
	if [ "$Opta" -a "${Url%%:*}" != http ]
	then
		put_dir "$Url" user
	fi
}

cmd wget '[-o Http_debug=1] url'
wget()
{
	local nredir=0
	Http_redirect_url=$1
	[ "$1" ] || { error "wget: internal error: missing argument"; return 1; }
	[ "$Optstr" ] && eval "$Optstr"
	while [ "$Http_redirect_url" -a $nredir -le 5 ]
	do
		[ $nredir -gt 0 ] && Connected_host= Connected_port=
		nredir=$(($nredir + 1))
		http_fill_request "$Http_redirect_url"
		http_connect || die "could not connect to $Host $Port"
		[ "$Http_debug" ] && printf %s "---------> To $Host:$Port
$http_req" >&2
		{
			printf %s "$http_req"
			[ "$Http_file" ] && cat $Http_file
			printf %s "$http_endreq"
		} >&3
		[ "$Http_debug" ] && echo "<--------- From $Host:$Port" >&2
		http_response <&4
	done
	[ $nredir -le 5 ] || error "too many redirections"
}

http_connect()
{
	local nhost nport tcp_pref
	[ "$Host" ] && nhost=$Host || die "http_connect: internal error: missing host"
	[ "$Port" ] && nport=$Port || die "http_connect: internal error: missing port"
	[ "$http_proxy" ] && nhost=${http_proxy%:*} nport=${http_proxy#*:}
	[ "$Connected_host" = "$nhost" ] &&
	[ "$Connected_port" = "$nport" ] && return
	# Check for /dev/tcp on portals
	case $Sys in
	(*BSD) tcp_pref=$(command mount | awk '$5 == "portal" {print $3}') ;;
	esac
	# If not found, check for shell providing /dev/tcp (ksh93, bash)
	[ "$tcp_pref" ] ||
	case "$Shellvers" in (bash\ *|ksh93\ *) tcp_pref=/dev ;; esac

	if [ "$tcp_pref" ]	
	then
		exec 3<>$tcp_pref/tcp/$nhost/$nport ||
		die "cannot connect to $nhost:$nport"
		exec 4<&3
	else	# create a pair of pipes, connect nc(1) coprocess to them
		[ "$(command -v nc)" ] || die "no command nc"
		[ -p /tmp/dim_in.$$ ] || mkfifo /tmp/dim_in.$$ /tmp/dim_out.$$
		trap "rm /tmp/dim_in.$$ /tmp/dim_out.$$" EXIT
		exec nc $nhost $nport </tmp/dim_in.$$ >|/tmp/dim_out.$$ &
		exec 3>|/tmp/dim_in.$$ 4</tmp/dim_out.$$
	fi
	Connected_host=$nhost Connected_port=$nport
}

# Usage http_fill_request url
# Initialize a request from given url
http_fill_request()
{
	local http_url=${1#http://} rbase fsize maxsize b
	rbase=${http_url#*/}
	[ "$rbase" = "$http_url" ] && rbase=
	Host=${http_url%/$rbase} 
	Port=${Host#*:}
	[ "$Port" = "$Host" ] && Port=80
	Host=${Host%:*}
	[ "$http_proxy" ] && rbase=$1
	case "$rbase" in ([!/]*|"") rbase=/$rbase ;; esac

	# The presence of CR chars (^M) at end of header lines is
	# required by the HTTP protocol, please do not remove them.
	
	[ "$Http_data" ] &&
	# Data not in URL are sent through a POST request
	http_req="POST $rbase HTTP/1.$Http_proto
User-Agent: $Dim_version
Host: $Host:$Port
Accept: */*
Content-Length: ${#Http_data}
Content-Type: application/x-www-form-urlencoded

$Http_data" ||
	# Default is a simple GET request
	http_req="GET $rbase HTTP/1.$Http_proto
User-Agent: $Dim_version
Host: $Host:$Port
Accept: */*

"
	#
	# If a file is specified, build a special file upload POST
	#
	[ "$Http_file" ] && {
		[ -f "$Http_file" ] ||
		die "invalid file $Http_file"
		fsize=$((354 + $(wc -c <$Http_file)))
		maxsize=2000000
		[ $fsize -gt $maxsize ] &&
		die "file $Http_file is larger than $maxsize"
		b=---------------------------60841378475689853717345751983
		http_req="POST $rbase HTTP/1.$Http_proto
User-Agent: $Dim_version
Host: $Host:$Port
Accept: */*
Content-Length: $fsize
Content-Type: multipart/form-data; boundary=$b

--$b
Content-Disposition: form-data; name=\"MAX_FILE_SIZE\"

$maxsize
--$b
Content-Disposition: form-data; name=\"userfile\"; filename=\"x\"
Content-Type: application/octet-stream

"
		http_endreq="
--$b--
"
	}
}

# Usage: http_response
# parse a HTTP response, and print content if OK
http_response()
{
	local out=1			# output is stdout
	local len=chuncked		# HTTP defaults to chuncked data
	local line http_line
	Http_redirect_url=
	while read -r line
	do
		[ "$Http_debug" = 1 ] && echo "$line" >&2
		line=${line%}		# strip CR
		case $line in
		(HTTP*2[0-9][0-9]*OK*)
			http_line=1 ;;
		(HTTP*30[12]*)			# redirect: output is null
			[ "$Http_debug" ] && out=2 || out=5 ;;
		(HTTP*[1345][0-9][0-9]*)	# output is stderr
			error $line; Connected_host= Connected_port= out=2 ;;
		(Connection:\ [Cc]lose)
			len=close; Connected_host= Connected_port= ;;
		(Content-Length:*)
			len=${line#*: } ;;	# read $len bytes
		(Location:*)
			Http_redirect_url=${line#*: } ;;  # redirect to rurl
		(*:*)
			;;
		(*)
			[ "$http_line" ] || continue
			case $len in
			(chuncked)
				[ "$out" = 2 ] && break
				# the next line is the data length in hexa
				read line
				len=$(printf %d 0x${line%})
				dd_read $len >&$out
				;;
			(close)
				cat >&$out ;;
			([0-9]*)
				dd_read $len >&$out ;;
			esac
			break
			;;
		esac
	done
	[ "$out" = 1 ]  # true if no error occured
}

# Usage: dd_read Nbytes
# Read the specified number of bytes from stdin and print them to stdout
dd_read()
{
	local bs=4096 len=$1 	# bs is the default block size
	local ddr nil n
	[ "$ddr" ] || { ddr=/tmp/dim_dd_read.$$; trap "rm -f $ddr" EXIT; }
	while [ $len -gt 0 ]
	do
		[ $len -ge $bs ] && b=$bs c=$(($len / $bs)) || b=$len c=1
		dd bs=$b count=$c 2>/dev/null | tee $ddr
		n=$(wc -c <$ddr)
		len=$(($len - $n))
	done
	[ "$Http_debug" ] && echo >&2
}

ned()
{
	awk 'BEGIN {file="'$1'"; co = 0}
        {
		action = substr($1, 1, 1)
		offset = 0 + substr($1, 2)
		count = 0 + $2
		if (action == "a") {
			while (co < offset) {getline < file; co++; print}
			for (i = 0; i < count; i++) {getline; print}
		} else {
			while (co < offset-1) {getline < file; co++; print}
			for (i = 0; i < count; i++) {getline < file; co++}
		}
	}
        END {while (getline < file > 0) print}
        '
}

# Usage: get_archive item vers
# Output: R1=arfile R2=ranc
get_archive()
{
	local arfile ranc item=$1 vers=$2
	for arfile in "$Archive"/$item/$item-$vers.*.*
	do
		case "$arfile" in
		(*\*|*--*.*.*)	break ;;	# not found
		(*.*.*)		R1=$arfile R2=; return 0 ;;
		esac
	done
	for arfile in "$Archive"/$item/$item-$vers[-.]*
	do
		case "$arfile" in
		(*\*)		return 1 ;;	# not found
		(*--*.*.*)	ranc=${arfile%.*.*}; ranc=${ranc#*--} ;;
		(*.*.*)		ranc= ;;
		(*)		die "no archive file: $arfile" ;;
		esac
		break
	done
	R1=$arfile R2=$ranc
}

# Usage: import_vers item vers
# FIXME: accept path as argument
# FIXME: add option to import directly in clone dir, bypassing job
import_vers()
{
	local item=$1 vers=$2 arfile ranc inflate extract f a2 la2 \
	      odir=$PWD v vv hist hist2
	local s1 s2 file cfile anc sum ldesc nindex aindex mod d noanc
	[ "$vers" = 0 ] && { mkitem $item; return 0; }

	[ -d "$Job/$item/.dim/$vers" -o -d "$Tmpdir/$vers" ] && return
	[ -f "$Job/$item/.removed/$vers" -a ! "$Optf" ] && {
		[ "$Optv" ] && echo "Skip removed version: $item-$vers" >&2
		return
	}
	get_archive $item "$vers" && arfile=$R1 ranc=$R2 || return
	[ "$ranc" ] && {
		import_vers $item $ranc || die "import failed: $item-$vers"
	}
	# Find out compress format from the name suffix
	[ "$Optv" -gt 1 ] && echo "extract $arfile" >&2
	case "$arfile" in
	(*.bz2)	inflate='bzip2 -cd' ;;
	(*.gz)	inflate='gzip -cd' ;;
	(*.Z)	inflate=zcat ;;
	(*)	die "unknown compress format: ${arfile##*.}" ;;
	esac

	# Find out archive format from the name suffix
	case "$arfile" in
	(*.tar.*) extract="tar xf -" ;;
	(*.dax.*) extract="dax -r" ;;
	(*)	  f=${arfile%.*}
		  die "unknown archive format: ${f##*.}" ;;
	esac

	[ "$Optn" ] && {
		mkdir -p "$Tmpdir/$vers"
		[ -f "cat/meta/index" ] && cp cat/meta/index "$Tmpdir/$vers"
	}
	rm -rf cat ned
	$inflate $arfile | $extract 

	if [ -d ned ]
	then
		# For each delta, grab ancestor corresponding file into cat/
		[ -d "$Job/$item/.dim/$ranc" ] && {
			[ "$Job/$item/.dim/$ranc/index" -ef cat/meta/index ] ||
			cp "$Job/$item/.dim/$ranc/index" cat/meta/index
			find ned/data ! -type d |
			awk '{print substr($0, 10)}' | {
				cd "$Job/$item/$item-$ranc"
				cpio -pdu "$Tmpdir/cat/data" 2>/dev/null
				cd "$odir"
			}
		}

		# Apply "diff -n" delta files using ned, starting with index.
		cp cat/meta/index cat/meta/index.prev
		ned cat/meta/index.prev <ned/meta/index >|cat/meta/index

		# Consistency check of index
		s1=$(openssl sha1 <cat/meta/index)
		s1=${s1#* }	# required for openssl-0.9.9
		lget "cat/meta/sum" && s2=$R1
		[ "$s1" = "$s2" ] || {
			[ "$Optn" ] || error "wrong index: $item-$vers"
		}

		# Get ancestor files for which permission needs to be changed
		checkindex cat/meta/index <cat/meta/index.prev |
		while read -r mod file
		do	case $mod in
			(+x|-x) d=${file%/*}
				[ "$d" != "$file" ] && mkdir -p "cat/data/$d"
				cp "$Job/$item/$item-$ranc/$file" \
				   "cat/data/$file"
				chmod $mod "cat/data/$file"
				;;
			esac
		done
		rm cat/meta/index.prev

		find ned/data ! -type d | awk '{print substr($0, 10)}' |
		while read -r file
		do
			cfile=$Tmpdir/cat/data/$file
			dfile=$Tmpdir/ned/data/$file
			ned "$cfile" <$dfile >$cfile.$$
			[ -x "$cfile" ] && chmod +x "$cfile.$$"
			mv "$cfile.$$" "$cfile"
		done
	elif [ ! "$Optn" ]	 # archive is absolute
	then
		[ -d "$Job/$item/.dim" ] || mkitem $item
		lget "$Tmpdir/cat/meta/hist" && hist=$R1
		lget "$Tmpdir/cat/meta/hist2" && hist2=$R1
		for v in $hist
		do	 # Adopt the closest existing ancestor in history
			[ -d "$Job/$item/.dim/$v" ] &&
			echo "$v" >|$Tmpdir/cat/meta/anc && break
		done
		for v in $hist $hist2
		do	# Inhibit import of previous versions.
			>|$Job/$item/.removed/$v
		done
	fi

	# Import result similarly to a clone
	lget cat/meta/anc && anc=$R1 || nousage=1 die "no import anc"
	lget cat/meta/sum && sum=$R1 || nousage=1 die "no import sum"
	lget "$Job/$item/.dim/$anc/desc" && ldesc=$R1 || ldesc=
	nindex=$Tmpdir/cat/meta/index
	aindex=$Job/$item/.dim/$anc/index
	[ "$Optn" ] && aindex=$Tmpdir/$anc/index || {
		mkdir -p "$Job/$item/.dim/$vers"
		cd cat/meta
		ln * "$Job/$item/.dim/$vers"
	}
	lnd "$Job/$item/$item-$anc" "$Job/$item/$item-$vers" \
	    "$aindex"
	[ "$Optv" -gt 1 ] && echo "delta from $Tmpdir to $item-$vers" >&2
	Cpio_link=l delta "$aindex" "$nindex" "$Tmpdir/cat/data" \
			   "$Job/$item/$item-$vers"

	[ "$Optn" ] && { cd $Tmpdir; echo $item-$vers; return; }
	
	# Allocate the version
	echo "$vers" >|$Job/$item/.sum/$sum
	[ -f "$Job/$item/.version/$vers" ] ||
	ln "$Job/$item/.dim/$vers/sum" "$Job/$item/.version/$vers"

	# Check if the version is a parent of already existing versions 
	# in the history of ancestor's descendants, and restore parentship.
	for v in $ldesc
	do
		gethist2 $item $v && hist2=$R1
		lin "$hist2" $vers && addanc2 $item $v $vers
		gethist $item $v && hist=$R1
		lin "$hist" $vers || continue
		# $vers becomes parent of $v 
		ldel "$Job/$item/.dim/$v/anc2" $vers
		ldel "$Job/$item/.dim/$vers/desc2" $v
		ldel "$Job/$item/.dim/$anc/desc" $v
		ladd "$Job/$item/.dim/$vers/desc" $v
		echo "$vers" >|$Job/$item/.dim/$v/anc
	done
	
	# Insert the version in the item ancestor tree
	ladd "$Job/$item/.dim/$anc/desc" $vers
	lget "$Job/$item/.dim/$vers/anc2" && lanc2=$R1 &&
	for v in $lanc2
	do
		isexp $v || continue
		[ -d "$Job/$item/.dim/$v" ] || {
			warn "skip missing ancestor2: $v"
			continue
		}
		# keep the anc2 graph reduced
		for vv in $ldesc
		do
			ldel "$Job/$item/.dim/$v/desc2" $vv
			ldel "$Job/$item/.dim/$vv/anc2" $v
		done
		[ -d "$Job/$item/.dim/$v" ] &&
		ladd "$Job/$item/.dim/$v/desc2" $vers
	done
	rm -f "$Job/$item/.removed/$vers"
	cd $Tmpdir
	echo $item-$vers
}

import_item()
{
	local item ivers i v curdir=$PWD
	Tmpdir=${Job%/*}/import.$$
	if [ "$1" ]
	then
		i=${1%%-*}
		item=$i
		ivers=${1#$item-}
		[ "$ivers" = "$item" ] && ivers=
	fi
	[ "$item" ] || die "no item"
	[ -d "$Archive/$item" ] || die "no item: $item"
	[ -d "$Job/$item" ] || {
		mkdir -p "$Job/$item" || die
		[ "$Optn" ] && trap "rmdir \"$Job/$item\"" EXIT
	}
	mkdir -p "$Tmpdir"
	cd $Tmpdir || die "cannot cd $Tmpdir"
	trap "rm -rf \"$Tmpdir\"" EXIT
	for v in $ivers "$Archive"/$item/$item-*--*.*.*
	do
		v=${v##*/}; [ "$v" = "$item-*--*.*.*" ] && continue
		v=${v#$item-}; v=${v%%--*.*.*}
		import_vers $item $v
		[ "$ivers" ] && break
	done
	cd "$curdir"
}

cmd import '[-anqv] [Item|Vers]' 'Extract archives into versions'
import()
{
	local i item
	if [ "$1" ]
	then
		for i
		do
			(import_item $i)
		done
		return
	else
		getiv && item=$R1
		[ "$item" ] && {
			import_item $item
			return 
		}
	fi
	for i in $Archive/*
	do
		[ "$i" = "$Archive/*" ] && continue
		(import_item ${i##*/})
	done
}

cmd umount '[-nvq] Vers|Dir' 'Unmount the version Vers or Dir'
umount() { remove $@; }

# FIXME: propagate the log to descendants
#cmd remove '[-npq] Vers' 'Remove the version Vers (dangerous)' \
#'	Remove a version Vers from library. If the version has been
#	exported or is a clone directory, then the version can be
#	restored, as no data is suppressed, otherwise its removal
#	can not be undone.
#
#	In the current state, this command is dangerous and produces
#	irreversible loss of data. Do not use it.'
cmd remove
remove()
{
	local item vers clonepref p anc i descs d hist 
	[ $# = 0 ] && die "missing version"
	[ $# -gt 1 ] && die "too many arguments"
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version: $1"
	isclone "$vers" && clonepref=+
	[ "$Cmd" = umount -a ! "$clonepref" ] && die "invalid clone directory"
	[ "$Optv" ] && {
		if [ "$clonepref" ]
		then
			lget "$Job/$item/.dim/$vers/path" && p=$R1
			echo "umount $p ($item$vers)" >&2
		else
			echo "removing $Job/$item/$item-$vers" >&2
		fi
	}
	[ "$Optn" ] && return
	lget "$Job/$item/.dim/$vers/anc" && anc=$R1
	lget "$Job/$item/.dim/$vers/anc2" && anc2=$R1
	lget "$Job/$item/.dim/$vers/desc" && descs=$R1
	for i in $anc2
	do
		ldel "$Job/$item/.dim/$i/desc2" $vers
	done
	ldel "$Job/$item/.dim/$anc/desc" $vers
	if [ "$clonepref" ]
	then
		rm -r "$Job/$item/$item$vers"  # remove clone index tree
	else	
		# Remove a version already saved or exported
		isexp $vers && {
			>|$Job/$item/.removed/$vers
			gethist $item $vers && hist="$vers $R1"
			gethist2 $item $vers && hist2="$R1"
		}
		for d in $descs
		do
			# reparent descendants to ancestor
			echo $anc >|$Job/$item/.dim/$d/anc
			ladd "$Job/$item/.dim/$anc/desc" $d
			# Save history of removed version in descendants
			[ "$hist" ] && echo $hist >|$Job/$item/.dim/$d/hist
			[ "$hist2" ] &&
			echo $hist2 >|$Job/$item/.dim/$d/hist2
			for i in $anc2
			do
				addanc2 $item $d $i
			done
		done
		lget "$Job/$item/.dim/$vers/sum" && rm "$Job/$item/.sum/$R1"
		rm -r "$Job/$item/$item-$vers" # remove data directory
	fi
	rm -r "$Job/$item/.dim/$vers"
}

# Set global variables Job, Archive and Url
setjob()
{
	getdir "$1" || die "invalid directory: $1"
	while [ "$R1" ]
	do
		if [ -f "$R1/.dimlib/url" ]
		then
			export Job=$R1/.dimlib/item
			Archive=$R1/.dimlib/archive
			read Url <$R1/.dimlib/url
			return 0
		fi
		R1=${R1%/*}
	done
	unset Job Archive Url
	return 1
}

cmd item '[-a] Path' 'Print the item containing Path'
item()
{
	local d item curdir=$PWD
	getiv "$1" && item=$R1
	if [ "$Opta" ]
	then
		cd "$Archive"
		for d in *
		do
			[ "$d" != "*" ] && echo "$d"
		done
		cd "$curdir"
	else
		[ "$item" ] && echo "$item"
	fi
}

cmd job '[-l] [Path]' 'Print the job containing Path' \
'	Print the job containing Path.
	With -l, print also the Url of the library.'
job()
{
	setjob "$1" || return
	[ "$Optl" ] && echo "${Job%/.dimlib/item} served by $Url" ||
	echo "${Job%/.dimlib/item}"
}

cmd lib '[Path]' 'Print the Url of the library'
lib()
{
	setjob "$1" || return
	echo $Url
}

cmd version '[-apsS] [Vers|Dir]' 'Print item version info'
version()
{
	local item v vers
	getiv "$1" && item=$R1 vers=$R2
	[ "$item" ] || die "no item"
	if [ "$Opta" ]
	then
		cd "$Job/$item/.dim"
		for v in *
		do
			case "$v" in
			("*"|.|..)	continue ;;
			(+*|*--*)	[ "$Opte" ] && continue ;;
			esac
			pversion $item $v 
		done
	elif [ ! "$1" ]
	then
		[ "$vers" ] || die "invalid version: ${1:-.}"
		Narg=${N1:-0} getanc $item $vers && vers=$R1
		pversion $item $vers
	else
		for v
		do
			getiv "$v" && item=$R1 vers=$R2
			[ "$vers" ] || error "no version found: $v"
			[ "$Opte" ] && { isexp $vers || continue; }
			pversion $item $vers
		done
	fi
}

cmd tarball '[-nqv] Vers' 'Generate a gzipped tar file of Vers'
tarball()
{
	local item vers d pvers curdir=$PWD
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version: ${1:-.}"
	Opts= OptS= Optp= getpvers $item $vers && pvers=$R1
	cd "$Job/$item"
	[ "$Optn" ] && d=/dev/null || d=$curdir/$pvers.tgz
	tar chz${Optv:+v}f $d $pvers/
	echo "$curdir/$pvers.tgz"
}

# FIXME: should fail if version has descendants.
# FIXME: should fail if user is not the version author
cmd unexport
unexport()
{
	local item vers pvers anc anc2 pa sum umail sign res sfile darfile
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version: ${1:-.}"
	isclone "$vers" && getanc $item $vers && vers=$R1
	getpvers $item $vers && pvers=$R1
	isexp $vers || die "version not exported: $pvers"
	getanc $item $vers && anc=$R1
	getanc2 $item $vers && anc2=$R1
	[ ! "$Optn" ] && [ "$anc2" ] &&
	for pa in $anc2
	do
		rmanc2 $item $vers $pa
	done
	OptS=1 getpvers $item $vers && sum=$R1
	case "$Url" in
	(http:/*)
		umail=$(printf %s $Email | urlenc)
		sign=$(printf %s $sum | sign2)
		res=$(wget "$Url?uv=1&m=$umail&p=$sign&i=$item&v=$sum&n=$Optn")
		;;
	(file:/*)
		sfile=${Url#file:}/item/$item/.sum/$sum
		lget "$sfile" && res=$R1
		[ "$vers" = "$res" ] && [ ! "$Optn" ] &&
		rm "sfile" "${Url#file:}/item/$item/.version/$res"
		;;
	(rsync:*)
		lget "$Job/$item/.sum/$sum" && res=$R1
		[ "$res" = "$vers" ] && [ ! "$Optn" ] &&
		rsync --remove-source-files \
		      ${Url#rsync:}/item/$item/.sum/$sum \
		      "$Job/$item/.sum/$sum" &&
		rsync --remove-source-files \
		      ${Url#rsync:}/item/$item/.version/$vers \
		      "$Job/$item/.version/$vers" &&
		rm "$Job/$item/.version/$vers"
		;;
	esac
	[ "$vers" = "$res" ] || die "server return: $res"
	darfile=$item-$vers--$anc.$Ar_ext
	[ "$Optv" ] && echo "$pvers unexported" >&2
	[ "$Optv" ] && echo "remove $Archive/$item/$darfile" >&2
	[ "$Optn" ] || rm "$Archive/$item/$darfile" 
	remove $vers
}

cmd index '[-f] [Vers]' 'Print index of Vers'
index()
{
	local item vers d
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version: ${1:-.}"
	case $vers in
	(+*)	
		Optp=1 getpvers $item "$vers" && d=$R1
		[ "$Optf" ] && rm -f "$Job/$item/.dim/$vers/scan"
		cd $d && update_sum $item ${vers#+} ;;
	esac
	awk '{print substr($0, 44)}' "$Job/$item/.dim/$vers/index"
}

cmd client '[-ap] [Vers]' 'Print clients of Vers'
client()
{
	local f p o max list cld vers curdir=$PWD
	getiv "$1" && vers=$R2 cld=$R3
	[ "$vers" ] || die "invalid version: ${1:-.}"
	cd "$Job"
	if [ "$Opta" ]
	then
		for f in */.dim/+*/path
		do
			[ "$f" = "*/.dim/+*/path" ] && return
			lget "$f" && p=$R1
			case $cld in
			($p/*)
				f=${f%/path}
				echo "${Optp:+$Job/}${f%%/*}${f##*/}" ;;
			esac
		done
	else
		o=$IFS max=0
		for f in */.dim/+*/path
		do
			[ "$f" = "*/.dim/+*/path" ] && return
			lget "$f" && p=$R1
			case $cld in
			($p/*)
				IFS=/; set -- $p; IFS=$o
				if [ $# -gt $max ]
				then
					max=$# list=${f%/path}
				elif [ $# = $max ]
				then
					list="$list ${f%/path}"
				fi
				;;
			esac
		done
		[ $max != 0 ] &&
		for f in $list
		do
			echo "${Optp:+$Job/}${f%%/*}${f##*/}"
		done
	fi
	cd "$curdir"
}

cmd supplier '[-ap] [Vers]' 'Print suppliers of Vers'
supplier()
{
	local f p cld o min list vers curdir=$PWD
	getiv "$1" && vers=$R2 cld=$R3
	[ "$vers" ] || die "invalid version: ${1:-.}"
	cd "$Job"
	if [ "$Opta" ]
	then
		for f in */.dim/+*/path
		do
			[ "$f" = "*/.dim/+*/path" ] && return
			lget "$f" && p=$R1
			case $p in
			($cld/*)
				f=${f%/path}
				echo "${Optp:+$Job/}${f%%/*}${f##*/}" ;;
			esac
		done
	else
		o=$IFS min=9999
		for f in */.dim/+*/path
		do
			[ "$f" = "*/.dim/+*/path" ] && return
			lget "$f"
			case $p in
			($cld/*)
				IFS=/; set -- $p; IFS=$o
				if [ $# -lt $min ]
				then
					min=$# list=${f%/path}
				elif [ $# = $min ]
				then
					list="$list ${f%/path}"
				fi
				;;
			esac
		done
		[ $min != 9999 ] &&
		for f in $list
		do
			echo "${Optp:+$Job/}${f%%/*}${f##*/}"
		done
	fi
	cd "$curdir"
}

cmd list '[Vers]' 'Print versions of files'
list()
{
	local item vers ancs sum last a rancs pa
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version: ${1:-.}"
	Opta=1 getanc $item $vers && ancs=$R1
	isclone "$vers" && update_sum $item ${vers#+} && sum=$R1
	getpvers $item $vers && last=$R1

	# Revert ancestor list in rancs
	for a in $ancs; do rancs=$a" "$rancs; done
	for a in $rancs $vers
	do
		getpvers $item $a && pa=$R1
		echo $pa
		cat "$Job/$item/.dim/$a/index"
	done |
	awk -v last=$last '
	NF == 1 {
		v = $0
		if ((len = length(v)) > vl) vl = len
		next
	}
	{
		fname = substr($0, 44)
		if (! fname in sum || $2 != sum[fname]) {
			sum[fname] = $2
			vers[fname] = v
		}
	}
	v == last {printf "%-" vl "s  %s\n", vers[fname], fname} 
	'
}

cmd label '[-sSp] [-0-9] File' 'Print version for each line for File'
label()
{
	local file item vers rdir rfile ancs max n node a ad af
	case "$1" in
	("") die "missing file" ;;
	(./*|../*|/*) file=$1 ;;
	(*) file=./$1 ;;
	esac
	[ -f "$1" ] || die "invalid file $file"
	getiv "$file" && item=$R1 vers=$R2
	[ "$vers" ] || die "no version for file $file"
	Opts= OptS= Optp=1 getpvers $item $vers && rdir=$R1
	relpath "$file" "$rdir" && rfile=$R1
	istextfile "$file" || die "binary file: $file"
	Opta=1 getanc $item $vers && ancs=$R1
	getpvers $item $vers && node=$R1
	max=$N1 n=0
	for a in $ancs
	do
		echo "node $node"
		getpvers $item $a && node=$R1
		Opts= OptS= Optp=1 getpvers $item $a && ad=$R1
		af=$ad/$rfile
		[ -f "$af" ] || af=/dev/null
		diffcmd "$af" "$file" |
		awk '/^[0-9]/ {sub(/^.*[acd]/, ""); print}'
		[ "$max" ] && {
			[ $n -ge $max ] && break
			n=$(($n + 1))
		}
	done |
	awk -v file="$file" '
	/^node / {
		name = $2; tlen = length(name)
		if (tlen > len) len = tlen
		next
	}
	/,/ {
		split($0, v, ",")
		for (i = v[1]; i <= v[2]; i++)
			if (!lab[i]) lab[i] = name
		next
	}
	{ if (!lab[$1]) lab[$1] = name }
	END {
		len += (8 - (len+2) % 8)	# to preseve tabs
		while (getline <file > 0) {
			n = n + 1
			printf("%-" len "s |%s\n",
			       lab[n] ? lab[n] : name, $0)
		}
	}
	'
}

# Usage: getcmd string
# Output: R1=Cmd
getcmd()
{
	local ambig res c
	lin "$Cmds" $1 && { R1=$1; return 0; }
	R1= ambig=0
	for c in $Cmds
	do
		case $c in
		("$1"*)	[ "$R1" ] && ambig=1 R1="$R1 $c" || R1=$c ;;
		esac
	done
	[ "$R1" ] || ambig=1 R1=$1
	return $ambig
}

# Return the dim library of a clone directory
libdir() { :; }

exit_actions()
{
	rm -f /tmp/dim_in.$$ /tmp/dim_out.$$ /tmp/nlist.$$
}

# Usage: parseopt arguments
parseopt()
{
	local nind=0 opt
	Optind=1 OPTIND=1
	case $Shellvers in (bash*|ksh88|ksh93*|zsh*) nind=1 ;; esac
	while getopts :0123456789aAB:bcC:D:efHi:IlMnNOo:pqsSU:Vvw opt
	do
		case $opt in
		([0-9])
			case $Shellvers in
			(bash*|ksh93*)
				eval N$nind=\${N$nind}$opt
				[ $Optind = $OPTIND ] || nind=$(($nind + 1))
				;;
			(*)
				[ $Optind = $OPTIND ] || nind=$(($nind + 1))
				eval N$nind=\${N$nind}$opt
				;;
			esac
			;;
		([aAbcefHIlMnNOpSsw]) eval Opt$opt=1 ;;
		(B)	Optb=1 Branch=$OPTARG ;;
		(C)	DIMRC=$OPTARG ;;
		(D)	cd "$OPTARG" || die "invalid directory" ;;
		(o)	Optstr="$Optstr $OPTARG" ;;
		(i)	Iarg="$Iarg $OPTARG" ;;
		(q)	Optv= ;;
		(U)	Url=$OPTARG ;;
		(V)	echo $Dim_version; exit ;;
		(v)	Optv=1$Optv ;;
		(*)	[ "$Cmd" ] && Help=1 die; pusage; exit ;;
		esac
		Optind=$OPTIND
	done
	Optind=$OPTIND
	Optstr=${Optstr# }
}

#
# main starts here
# Init global options and flags
#
Dim_version="$Dim_version $(uname -mrs) $Shellvers"
Ar_ext=tar.gz Ar_cmd='tar czf -'	# Default archiving command
Zerosum=da39a3ee5e6b4b0d3255bfef95601890afd80709	# sha1sum of ""
H='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
Sha1sum_pattern=$H$H$H$H$H$H$H$H	# 40 hexa digits
Optv=1					# default verbosity level
exec 5>/dev/null			# for wget
Http_debug= Http_proto=1
Connected_host= Connected_port=

parseopt "$@"
DIMRC=${DIMRC:-$HOME/.dimrc}		# default configuration file
[ -f "$DIMRC" ] && . "$DIMRC" && unset Rsa_key 
setjob					# set Job, Archive and Url
shift $(($Optind - 1))
[ $# = 0 ] && { pusage; exit; }		# dim requires a subcommand
getcmd "$1" || {
	Cmd=$R1
	case "$Cmd" in
	(*" "*)	error "ambiguous command \"$1\": $Cmd" ;;
	(*)	error "invalid command: $Cmd"; pusage ;;
	esac
	exit 1
}
Cmd=$R1
shift
parseopt "$@"
shift $(($Optind - 1))

[ "$Cmd" = export ] && _Cmd=do_export || _Cmd=$Cmd	# to avoid builtin

$_Cmd "$@"

exit_actions
