#!/bin/sh

#
# Copyright (c) 2007, 2008, 2009 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.4-mv9

# 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

# Use obsolete shell syntax for compatibility with ancient shells until
# we are certain that the shell is modern enough or exit properly.

# 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 { unset OLDPWD; cd .; [ $OLDPWD ]; }
	then
		R1="dash"		# debian almquist shell
	elif [ "`{ : ${_z_?1}; } 2>&1`" = 1 ]
	then
		R1="sh ash/BSD"		# original almquist shell or BSD /bin/sh
	elif { echo 1 | read R1; [ "$R1" ]; };
	then
		R1="ksh88"
	else
		R1="unknown shell"	# unsupported shell.
	fi
}

# Usage: check_dev_tcp
# Output: R1 contains a network error message
# Return 0 if /dev/tcp is OK, 1 otherwise.
# Try to connect to a known tcp port on localhost using /dev/tcp as
# normally supported by bash or ksh93. If command is ok or error is
# "Connection refused" then /dev/tcp is valid.
check_dev_tcp()
{
	R1=`exec 2>&1; echo hello >/dev/tcp/127.0.0.1/8 && echo Connection ok`
	case $R1 in
	*Connection*)	return 0 ;;
	*)		return 1 ;;
	esac
}

# Shell interpreter portability init actions, to have a consistent
# behaviour accross different shells.
Sys=`uname -s`
getshell && Shellversion=$R1
case $Shellversion in
unknown\ shell)
        echo "Shell interpreter 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
	check_dev_tcp && Valid_dev_tcp=1 Shellversion="$Shellversion /dev/tcp"
	;;
ksh93\ *)
	# Substitute 'f()' by 'function f' in this script, then re-run.
	[ ! "$Ksh" ] && {
		case $Sys in
		CYGW*)	Shell=`ps -p $$ | awk '$2 ~ /[0-9]+/ {print $NF}'` ;;
		Darwin)	Shell=`ps -p $$ -o command=` ;;
		*)	Shell=`ps -p $$ -o args=` ;;
		esac
		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
	check_dev_tcp && Valid_dev_tcp=1 Shellversion="$Shellversion /dev/tcp"
	;;
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
	;;
esac
[ "$Shellversion" = ksh88 ] && set -o noclobber || set -C	# no clobber

# From here, we can use modern shell syntax.

# 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.
Cmdlist=" "	# global list of commands
cmd() { Cmdlist="$Cmdlist$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 [-aAbcefInpqsMRSVv] [-C Config] [-D Dir] [-U Url] [-o Opt]
      [-B Version] [-i File] [-0-9] Cmd [Item|Version|Path...]
DESCRIPTION
  Dim is a version control system. It creates, clones, archives,
  lists, replicates, extracts, prints differences, merges, ensures
  authenticity and integrity of versions.

  Dim extends the traditional file system storage model to provide
  additional concepts relevant to configuration management, and the
  corresponding attributes, metadata, and commands. An item contains
  all the data together necessary to the component, as files or
  directories. The item is what versioning is applied to. Items can be
  composed (to be detailed). A version is an instance of an item uniquely
  defined by its content. A clone is a version under modification. A
  library contains the set of all immutable exported versions and user
  profiles. A library is associated with a unique namespace for items,
  versions and users. A job is the working space in the user machine where
  all commands can be run from, to replicate a library, browse versions,
  create, modify, save and export clones.

  Dim generates version names automatically, using the item name, the
  user alias, and a generated number. Several categories of version
  exist:
  - Main versions: the item name followed by a sequence of numbers
    and dots,
  - User versions: a main version name, as above, followed by "-" then
    the user alias,
  - Local versions: a main or user version, as above, followed by "--"
    then a sequence of numbers and dots.

  Main and user versions are generated centrally by the dim server,

OPTIONS
  Options can specified before or after Cmd. Options not applicable
  to Cmd are silently ignored.
  -a		Print all (ancestor, descendant, version, clone, item).
  -A		Generate absolute archive (export).
  -b		Fork a new branch (save and export).
  -B Version	Fork a new branch named Version (save and export).
  -c		Report only files with unresolved conflicts (diff).
  -C Config	Set configuration file to Config instead of ~/.dimrc.
  -D Dir	Use directory Dir, instead of default $PWD.
  -e		Select the first exported ancestor (diff, ancestor).
  -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). With both -N and -O,
		shown only the words added and removed.
  -n		Dry run mode. Simulate actions but not perform them.
  -o Opt	Set specific command options in string Opt.
  -p		Print version path.
  -q		Quiet mode.
  -R 		Recurse in sub-directories.
  -S 		Print the sha1 checksum of the version data.
  -s		Print short version name.
  -U Url	Use Url instead of default (get, put).
  -v		Verbose mode. Print detailed informations on /dev/stderr.
  		If repeated, increase verbosity level.
  -V 		Print the dim version and exit.
COMMANDS
'"$(for c in $(for c in $Cmdlist; do echo $c; done | sort)
  do
	man_cmd $c
  done)"'
EXAMPLES
  Create a new job and track an existing repository:

    $ mkdir jobdir
    $ cd jobdir
    $ dim mkjob http://www.oligem.com/dim/wdim.php
    $ dim get -a		# download archives
    $ dim import -a		# expand archives

  Query some informations about items and versions:

    $ dim item -a		# list all items
    $ dim version -a item	# list all versions of item

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.
  jobdir/.dimlib/
  		The directory .dimlib is created under jobdir and
		contains all the data and metadata of existing
		versions.

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.
  - 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), diff3(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 $Cmdlist
	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 ancestor
  -b,-M,-B Version 	Branch flags: force a branch, main, named
  -C Conf,-D Dir,-U Url	Override defaults
Arguments:
  Version	Any of directory or file path, version, item-version
  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
# Return the first line of file.
lget() { R1=; [ -f "$1" ] && read -r R1 <$1; [ "$R1" ]; }

# Usage lset file text
# Store text in file
lset() { echo "$2" >|$1; }

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

# Usage: ladd file value...
# Append list of values to the list stored in file.
# Avoid to duplicate values in list.
ladd()
{
	local file=$1 word list
	[ "$2" ] || die ladd: internal error: missing argument
	[ -f "$file" ] && read -r list <$file || list=
	for word in $2
	do
		lin "$list" $word || list="$list $word"
	done
	echo ${list# } >|$file
}

# Usage: ldel file list
# Remove each word in list from the first line of file.
ldel()
{
	local file=$1 word oword list olist
	[ "$2" ] || die ldel: internal error: missing argument
	[ -f "$file" ] || return
	read -r olist <$file
	for oword in $olist
	do
		for word in $2
		do
			[ "$oword" = "$word" ] && continue 2
		done
		list="$list $oword"
	done
	echo ${list# } >|$file
}

getattr() { lget "$Job/$2/.dim/$3/$1"; }
setattr() { lset "$Job/$2/.dim/$3/$1" "$4"; }
addattr() { ladd "$Job/$2/.dim/$3/$1" "$4"; }
delattr() { ldel "$Job/$2/.dim/$3/$1" "$4"; }

cmd dag '[V1 [V2]]' '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 V1 is specified, then the graph is rooted at it.
	If version V2 is specified, all nodes found after V2
	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" ] && getanc $item $a $N1 && a=$R1
		a=${a:-0}
	fi
	if [ "$2" -o "$N2" ]
	then
		getiv "${2:-$1}" && e=$R2 || die "invalid version"
		getanc $item $e $N2 && e=$R1
	fi
	getiv && isclone "$R2" && item=$R1 nclone=${R2#+}
	{ echo "digraph $item {"
	while [ "$a" ]
	do
		for v in $a
		do
			[ "$v" != "+$nclone" ] && echo "\"$v\";" ||
			echo "\"$v\" [color=lightgrey style=filled ];"
			d=
			getattr desc $item $v && d=$R1 &&
			for vv in $d
			do
				echo "\"$v\" -> \"$vv\";"
			done
			getattr desc2 $item $v && 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 && break 2
			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: isarchived item version
isarchived() { [ -f "$Archive/$1/$1-$2".*.* -o -f "$Archive/$1/$1-$2--"* ]; }

# Usage: getiv string
# Input: N1
# Output: R1=item R2=version R3=versiondir R4=clonedir R5=entry
# Find the version matching string.
# String can be path, item, version, or item and version.
# Return item, version, versiondir, clonedir, entry.
# Die if specified item is not found.
getiv()
{
	local item v1 v2 version versiondir clonedir symlink dir N1=${N1:-0}
	[ "$1" ] || set .
	case $1 in	# prefix first digit with -
	([0-9]*) set -- "-$1" ;;
	esac
	case $1 in
	(*/*:*|.:*|..:*)
		die "invalid version ${1%%:*}"
		;;
	(*:*)		# item + version : entry
		getiv "${1%:*}" && R5=${1#*:} && return
		;;
	(*/*|.|..)	# path
		[ "$Jobdir" ] || nousage=1 die no job
		# find the closest clonedir in path
		getdir "$1" && dir=$R1 f=$R2
		while [ "$dir" ]
		do
			for symlink in $Job/*/.dim/+*/dir
			do
				[ -L "$symlink" ] || break 2
				[ "$symlink" -ef "$dir" ] || continue
				clonedir=$dir
				break 2
			done
			dir=${dir%/*}
		done
		if [ "$clonedir" ]
		then
			symlink=${symlink#$Job/}
			item=${symlink%%/*}
			symlink=${symlink%/dir}
			version=+${symlink##*+}
		fi
		;;
	([a-zA-Z]*)	# item + version
		item=${1%%[-+]*}
		[ -d "$Archive/$item" -o -d "$Job/$item" ] ||
		nousage=1 die "invalid item $item"
		[ -d "$Job/$item/$1" ] && version=${1#$item} || {
			v1=${1#$item}; v2=${v1#-}
			[ "$v2" ] && isarchived $item $v2 &&
			(cd_import_tmpdir; Optv= import_version $item $v2) &&
			[ -d "$Job/$item/$item$v1" ] && version=$v2
		}
		;;
	([-+][0-9]*)	# version
		getiv && item=$R1
		[ -d "$Job/$item/$item$1" ] && version=$1 || {
			isarchived $item $1 &&
			(cd_import_tmpdir; Optv= import_version $item $1) &&
			[ -d "$Job/$item/$item$1" ] && version=$1
		}
		;;
	(*)	die "invalid id $1"
		;;
	esac
	if [ "$version" ]
	then
		[ "$N1" != 0 ] && Opta= getanc $item ${version#-} $N1 &&
		version=-$R1
		versiondir=$Job/$item/$item$version
		isclone $version && [ ! "$clonedir" ] &&
		getclonedir $item $version && clonedir=$R1
	fi
	R1=$item R2=${version#-} R3=$versiondir R4=$clonedir
}

# Usage: getiv2 [[old] new]
# Input: N1 N2
# Output: R1=item R2=v1 R3=v2 R4=vdir1 R5=vdir2
getiv2()
{
	local item v1 v2 vdir1 vdir2
	[ $# -gt 2 ] && die too many arguments
	[ "$N3" ] && die "too many -<n> flags"
	if [ "$2" ]
	then
		[ "$N1" ] && die "too many -<n> flags"
		getiv "$1" && item=$R1 v1=$R2 vdir1=$R3
		[ "$v1" ] || die "invalid version $1"
		[ "$item" ] || nousage=1 die no item
		getiv "$2" && v2=$R2 vdir2=$R3
		[ "$v2" ] || die "invalid version $2"
		[ "$item" = "$R1" ] ||
		die "versions $item-$v1 and $R1-$v2 belong to different items"
	else
		[ "$1" -a "$N2" ] && die "too many -<n> flags"
		N1=${N1:-1} getiv "$1" && item=$R1 v1=$R2 vdir1=$R3
		[ "$item" ] || nousage=1 die no item
		[ "$v1" ] || die "invalid version $1"
		N1=$N2 getiv "$1" && v2=$R2 vdir2=$R3
		[ "$v2" ] || die "invalid version $2"
	fi
	R1=$item R2=$v1 R3=$v2 R4=$vdir1 R5=$vdir2
}

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

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

# Usage: getdir path
# Ouptut: R1=dir R2=file
getdir()
{
	local opwd=$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
	elif [ -d "$1" ]
	then
		cd "$1" 2>|/dev/null || die cannot chdir to "$1"
	else
		cd "${1%/*}" 2>|/dev/null || die cannot chdir to "${1%/*}"
	fi
	R1=$PWD R2=$f
	cd "$opwd"
}

# Usage: getanc item version narg
# Input: Opte OptM
# Output: R1=anc (or anclist if -a)
getanc()
{
	local i anc anclist item=$1 version=$2 narg=$3
	[ "$item" -a "$version" ] ||
	die getanc: internal error: missing item or version
	[ $version = 0 ] && { R1=0; return; }
	getattr anc $item $version && anc=$R1 ||
	die "$version has no ancestor"
	[ "$Opte" ] && anc=${anc%%--*}
	[ "$OptM" ] && anc=${anc%%-*}
	case $narg in
	(0)	anc=$version ;;
	(''|1)	;;
	(*)	i=1
		while [ $i -lt $narg ]
		do
			getattr anc $item $anc && anc=$R1
			i=$(($i + 1))
		done
		;;
	esac
	[ ! "$Opta" ] && R1=$anc && return
	anclist=$anc
	while [ "$anc" -a "$anc" != 0 ]
	do
		getattr anc $item $anc && anc=$R1 || break
		anclist="$anclist $anc"
	done
	R1=$anclist
}

cmd ancestor '[-aeHlOpSs] [V1 [V2]]' 'Print the ancestor of V1' \
'	Print the ancestor of V1. If V2 is specified,
	print the closest common ancestor of V1 and V2.
	With -e, print the exported ancestor.
	With -a, print all ancestors, up to version 0.
	With -H, print all historical ancestors, (i.e.
	print also ancestors not yet imported, or removed).
	With -l, print all ancestors up to the exported ancestor.
	With -O, print the lists of other merged ancestors.
	With -<n>, where n is a number, print the n'"'"'th ancestor.'
ancestor()
{
	local v v1 v2 item
	N1= 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
		return
	fi
	if [ "$Optl" ]
	then
		getlocalanc $item $v1
	elif [ "$OptO" ]
	then
		ancestor2 "$@"
		return
	elif [ "$OptH" ]
	then
		gethist $item $v1
	else
		getanc $item $v1 ${N1:-1}
	fi
	for v in $R1
	do
		pversion $item $v
	done
}

# Usage: anc2 itemversion
# Print the list of ancestor2.
ancestor2()
{
	local v a a2 item a1
	getiv "$1" && item=$R1 v=$R2
	[ "$v" ] || die "invalid version ${1:-.}"
	if [ "$OptH" ]
	then
		gethist2 $item $v && a2=$R1
	elif [ "$Opta" ]
	then
		getanc $item $v && a1=$R1
		getclosure anc $item $v && a2=$R1
	else
		getanc2 $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 v1
getanc2() { getattr anc2 $1 $2; }

# Usage: addanc2 item desc2 anc2
addanc2()
{
	local item=$1 desc2=$2 anc2=$3 a
	# Return silently if both versions do not exist
	[ -d "$Job/$item/.dim/$desc2" -a -d "$Job/$item/.dim/$anc2" ] || return
	# Do not add a version which is already our ancestor
	isanc2 $item $anc2 $desc2 && return
	getanc2 $item $desc2 && a=$R1
	for a in $a
	do	# Remove redundant edges
		isanc2 $item $a $anc2 && delanc2 $item $desc2 $a
	done
	addattr anc2 $item $desc2 $anc2
	addattr desc2 $item $anc2 $desc2
}

# Usage: delanc2 item desc2 anc2
delanc2()
{
	local item=$1 desc2=$2 anc2=$3
	delattr anc2 $item $desc2 $anc2
	delattr desc2 $item $anc2 $desc2
}

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

# Usage: gethist2 item v1
# Output: R1=history_ancestor2_list
gethist2()
{
	local item=$1 more=$2 new list res res2 next v1 v2 hist
	while [ "$more" ]
	do
		new=$more more=
		for v1 in $new
		do
			list=
			getattr hist $item $v1 ||
			getattr anc $item $v1 && list=$R1
			getattr hist2 $item $v1 ||
			getattr anc2 $item $v1 && list="$list $R1"
			for v2 in $list
			do
				lin "$res" $v2 ||
				res="$res $v2" more="$more $v2"
			done
		done
	done
	gethist $1 $2 && hist=$R1
	for v1 in $res
	do
		isexp $v1 || continue
		lin "$hist" $v1 && continue
		res2="$res2 $v1"
	done
	R1=$res2
}

cmd descendant '[-aepSs] [V1 [V2]]' 'Print descendants of V1'
descendant()
{
	local item v1 v2 ld v vv
	getiv "$1" && item=$R1 v1=$R2
	[ "$v1" ] || die "invalid version ${1:-.}"
	[ "$2" ] && {
		getiv "$1" && v2=$R2
		[ "$item" != "$R1" -o "$v2" = "" ] && die "invalid version $2"
	}
	while [ "$v1" ]
	do
		ld=
		for v in $v1
		do
			desc=
			getattr desc $item $v && 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" ] && v1=$ld || v1=
	done
}

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

# Usage: urlencode
# Encode stdin to be passed in HTTP request. Non alphanumeric characters
# are replaced by '%0', followed by their hexadecimal ASCII value.
urlencode()
{
	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 v1
# Create an absolute archive of v1, sent on stdout.
aar()
{
	local anc item=$1 v1=$2 f
	local tmpd=${Job%/*}/aar.$$
	local la la2 nla2 v

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

# Usage: dar item v1
# Create a delta archive, sent on stdout.
dar()
{
	local item=$1 v1=$2 anc f tmpd=${Job%/*}/dar.$$
	getattr anc $item $v1 && anc=$R1 || die "no ancestor"
	trap "cd /; rm -fr \"$tmpd\"" EXIT
	# diff relevant metadata
	cd "$Job/$item/.dim/$v1"
	mkdir -p "$tmpd/ned/meta" "$tmpd/cat/meta"
	[ -f anc2 ] && f=anc2
	[ -f log ] && f="$f log"
	cp $f anc sum date from sign $tmpd/cat/meta
	diffcmd -n ../$anc/index index >$tmpd/ned/meta/index

	# diff data
	mkdir -p "$tmpd/ned/data" "$tmpd/cat/data"
	cd "$Job/$item/$item-$v1"
	awk -v f1=../.dim/$anc/index -v f2=../.dim/$v1/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_getnewversion item sum anc
# Output: R1=version
http_getnewversion()
{
	[ "$Email" ] || die "no Email in $DIMRC"
	R1=$(Http_data="mail=$(printf %s $Email | urlencode)&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_getnewversion item sum anc
# Output: R1=version
file_getnewversion()
{
	local item=$1 sum=$2 anc=$3 furl=${Url#file:} num sfile version
	local itemdir=$furl/item/$item
	local sumdir=$itemdir/.sum versiondir=$itemdir/.version
	lget "$sumdir/$sum" && version=$R1 &&
	case $version in
	(*--*)	;;			# ignore local versions
	(*)	return ;;		# return existing version
	esac
	[ -f "${Job%/*}/user/$Email" ] ||
	nousage=1 die "Please run \"dim mkuser\" first."
	. "${Job%/*}/user/$Email"
	getnewversion $item $anc && version=$R1
	[ "$Optn" ] && return
	[ -d "$versiondir" ] || mkdir -p "$versiondir" || nousage=1 die
	[ -w "$versiondir" ] || nousage=1 die cannot write to "$versiondir"
	[ -d "$sumdir" ] || mkdir "$sumdir" || nousage=1 die
	[ -w "$sumdir" ] || nousage=1 die cannot write to "$sumdir"
	# Automatic branch creation if name collision
	while true
	do
		(echo $sum >$versiondir/$version) 2>|/dev/null && break
		num=${version##*[!0-9]}		# get last digits
		version=${version%$num}$(($num - 1)).1	# fork a branch
	done
	echo $version >|$sumdir/$sum
	R1=$version
}

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

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

cmd export '[-AIMbnqv] [-B Version]' 'Export the current clone to lib' \
'	Export the current clone to library. Create a public archive.
	Return a unique library wide version name.'
do_export()
{
	local item clone elog lv sum ev eanc anc desc arcmd arname
	local anc2 desc2
	[ $# -gt 0 ] && die too many arguments
	[ "$N1" ] && nousage=1 die "invalid flag -$N1"
	getiv && item=$R1 clone=$R2
	isclone "$clone" || die no clone

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

	# Save clone before exporting it
	lv=$(Cmd=save Opts=1 Optb= OptM= save)
	[ "$lv" ] || die "do_export: internal error: no version"
	isexp $lv && echo $item-$lv && return

	if [ "$Optn" ]
	then
		getattr sum $item $clone && sum=$R1
	else
		getattr sum $item $lv && sum=$R1
	fi || die "do_export: internal error: no sum"

	# Get a unique version name from the server
	eanc=${lv%--*}
	case $Url in
	(http:*)	http_getnewversion $item $sum $eanc && ev=$R1 ;;
	(file:*)	file_getnewversion $item $sum $eanc && ev=$R1 ;;
	(rsync:*)	rsync_getnewversion $item $sum $eanc && ev=$R1 ;;
	esac
	[ "$ev" ] || return
	[ "$Optv" ] && [ "$lv" != "$ev" ] &&
	echo "exporting $item-$lv as $item-$ev" >&2

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

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

	# Remove local ancestors since last export
	getlocalanc $item $lv && R1=anc
	for anc in $anc
	do
		unimport $item-$anc
	done

	# Update descendants and ancestors with new name.
	getattr anc $item $lv && anc=$R1
	delattr desc $item $anc $lv
	addattr desc $item $eanc $ev
	setattr anc $item $lv $eanc
	getattr desc $item $lv && desc=$R1
	for desc in $desc
	do
		setattr anc $item $desc $ev
	done

	# Update anc2 and corresponding desc2
	getattr anc2 $item $lv $anc && anc2=$R1
	for anc2 in $anc2
	do
		delattr desc2 $item $anc2 $lv
		addattr desc2 $item $anc2 $ev
	done
	getattr desc2 $item $lv && desc2=$R1
	for desc2 in $desc2
	do
		delattr anc2 $item $desc2 $lv
		addattr anc2 $item $desc2 $ev
	done

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

	# Rename local version into exported version
	mv "$Job/$item/$item-$lv" "$Job/$item/$item-$ev"
	mv "$Job/$item/.dim/$lv" "$Job/$item/.dim/$ev"

	# Add previous exported ancestor to anc2
	addanc2 $item $ev $anc

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

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

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

	# FIXME: should verify that archive is correct

	# Upload to library repository
	put_dir "$Url" archive/$item $item
}

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

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

# 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 version
# Verify the authentication signature using the RSA public key of author.
checksign()
{
	local item version sign from date sum res

	item=$1 version=$2
	isexp $version || return 2
	getattr sign $item $version && sign=$R1 || return 2
	# bug workaround: some signatures are prefixed with '(stdin)= '
	sign=${sign#*=?}
	getattr from $item $version && from=$R1 || return 2
	getattr date $item $version && date=$R1 || return 2
	getattr sum $item $version && sum=$R1 || return 2
	res=$(printf "%s\n%s\n%s\n%s\n" "$date" $from $sum $item-$version |
	      vsign ${Job%/*}/user/$from $sign)
	[ ${#Optv} -gt 1 ] && echo "$item-$version from $from, $date: $res" >&2
        case $res in
	(*\ OK)	res=0 ;;
	(*)	res=1 ;;
	esac
	return $res
}

cmd mkjob '[-nql] Url' 'Create a new job served by library Url' \
'	Changes the current directory into a job served by the library Url. \
	The library acts as a repository and a version server.'
mkjob()
{
	local url=$1 dir file
	[ $# -gt 1 ] && die too many arguments
	[ "$Jobdir" ] && nousage=1 die "$Jobdir is already a job"
	case $url in
	(http:*|rsync:*) ;;
	(file:.dimlib)	 ;;
	(file:/*)
		getdir ${url#file:} && dir=$R1 file=$R2
		[ "$file" ] && nousage=1 die "invalid url $url"
		case $dir in
		($PWD/.dimlib) ;;
		($PWD|$PWD/*)  nousage=1 die "invalid url $url
Use 'dim mkjob file:.dimlib' to create a job which is its own library" ;;
		esac
		;;
	(file:*)
		nousage=1 die "invalid url $url
File path must be absolute (or absent)"
		;;
	('')	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
	[ "$Optn" ] && return
	mkdir -p ".dimlib/archive" ".dimlib/item" ".dimlib/user" ||
	die "mkjob failed"
	echo "$url" >.dimlib/url || die "mkjob failed"
}

cmd mkitem '[-nqv] Item' 'Create a new item Item'
mkitem()
{
	local item=$1
	[ "$Jobdir" ] || nousage=1 die no job
	[ "$item" ] || nousage=1 die no item
	# Authorize only alphanum chars in item name
	case $item in (*[!A-Za-z0-9_]*) die "invalid item name $item" ;; esac
	[ "$Optn" ] && return

	# Create and populate item-0
	[ -f "$Job/$item/.dim/0" ] && return	# already in job

	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
	[ -f "$Job/$item/.version/0" ] && return # already in lib

	echo 0 >$Job/$item/.sum/$Zerosum
	setattr sum $item 0 $Zerosum
	ln "$Job/$item/.dim/0/sum" "$Job/$item/.version/0"
}

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" | urlencode)
		pubkey=$(openssl rsa -pubout <$DIMRC 2>|/dev/null)
		uname=$(printf %s "$name" | urlencode)
		ukey=$(printf %s "$pubkey" | urlencode)
		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: 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 version
# Output: R1=sum
# Compute sha1sum of current dir and store it in clone metadata
update_sum()
{
	local item=$1 version=$2 list f sum p clonedir opwd=$PWD
	local nscan=$Job/$item/.dim/$version/scan
	local nindex=$Job/$item/.dim/$version/index

	isclone $version ||
	die update_sum: internal error: $version is not a clone
	getclonedir $item $version && clonedir=$R1
	cd "$Job/$item/$item$version"
	getattr sum $item $version && sum=$R1
	if [ "$sum" -a -f "$nscan" ]
	then
		# Incremental mode
		list=/tmp/nlist.$$
		find . -type f \( $Fcnewer "$nscan" -o -links 1 \) >|$list
		[ -s "$list" ] || return 0 # no change since last scan
		>|$nscan
		while read -r f
		do
			# FIXME: encode filename
			[ "$f" -ef "$clonedir/$f" ] || {
				[ -f "$clonedir/$f" ] &&
				ln -f "$clonedir/$f" "$f" || {
					# remove file from index
					rm -f "$Job/$item/$item$version/$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
	cd "$opwd"
	R1=$(openssl sha1 <$nindex)
	R1=${R1#* }	# required for openssl-0.9.9
	setattr sum $item $version $R1
}

cmd add '[-nqv] Path...' 'Add Path to clone index' \
'	Add Path to clone index. Directories are added recursively.'
add()
{
	local path act d script versiondir findopt finddir rp item clonedir
	# normalize arguments: absolute
	for path
	do
		case $path in
		(/*)	set -- "$@" "$path" ;;
		(*)	set -- "$@" "$PWD/$path" ;;
		esac
		shift
	done
	for path
	do
		set --
		getiv "$path" && item=$R1 version=$R2 versiondir=$R3 clonedir=$R4
		[ "$clonedir" ] || die no clone
		if [ "$_Cmd" != del ]
		then
			t="rm -f \"$Job/$item/.dim/$version/scan\"; cpio -pdul"
			script="$t \"$versiondir\""
		fi
		case $_Cmd in
		(del)
			act=del findopt="( -depth ! -name . -print )"
			script="rm -f \"$Job/$item/.dim/$version/scan\"; xargs rm -r"
			finddir=$versiondir
			;;
		(*)
			act=add
			findopt="( -type f -print )"
			[ "$Fempty" ] &&
			findopt="$findopt -o ( -type d $Fempty ! -name . -print )"
			finddir=$clonedir
			# exclude suppliers clonedirs
			Optp=1 supplier >|/tmp/supplier.$$
			while read d
			do
				# normalize supplier: relative to finddir
				getclonedir ${d%+*} +${d##*+} && 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 to $finddir"
		# normalize argument: relative to clonedir
		relpath "$path" "$clonedir" && rp=$R1 || rp=
		[ "$rp" ] && path=./${rp%/} || path=.
		[ -e "$path" ] ||
		nousage=1 die "$finddir/${path#./}: No such file or directory"
		find "$path" -name .dimlib -prune "$@" $findopt |
		awk -v Optn="$Optn" -v Optv=${#Optv} -v \
			act=$act -v cmd=$Cmd -v script="$script" '
		$0 ~ /^\.\// { $0 = substr($0, 3) }
		{ if (! Optn) print $0 | script }'
	done
}

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

cmd check '[-aqv] V1' 'Check integrity of version V1'
check()
{
	local i v item v1 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%%+*} v1=+${v#*+}
				update_sum $item $v1 && sum=$R1
				;;
			(*)	item=${v%%-*} v1=${v#*-}
				checksign $item $v1
				case $? in
				(0) sres='sign=ok' ;;
				(1) sres='sign=bad' ;;
				(*) sres= ;;
				esac
				;;
			esac
			vcheck $item $v1 && cres=ok || cres=ok
			echo "$item-$v1 sum=$cres $sres"
		done
		[ "$Opta" ] && break
	done
}

# Usage: vcheck item version
# Return true if checksum and signature of itemvers is ok, false otherwise.
vcheck()
{
	local item=$1 refsum p pv isum odir sum
	getattr sum $item $2 && refsum=$R1
	Opts= OptS= Optp=1 getpversion $item $2 && p=$R1
	getpversion $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] V1' 'Merge V1 into current clone' \
'	Merge version V1 into current clone. The merge algorithm consists
	in finding the most recent common ancestor to V1 and the clone,
	identifying the changes between the common ancestor and V1, and
	applying those changes to the clone. The following rules solve
	conflicts:
	modification takes precedence over deletion and stability;
	directory creation takes precedence over file creation;
	deletion takes precedence over stability. The command "dim diff -c"
	list the files containing unresolved conflicts, which are delimited
	by the diff3(1) labels <<<<<<<, =======, ||||||| and >>>>>>>.'
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 no 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
	}
	getpversion $item +$nclone && p1=$R1
	getpversion $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
	getpversion $item $canc && p0=$R1
	[ "$Optv" ] && echo "$p0 is the common ancestor of $p1 and $p2" >&2
	Opts= OptS= Optp=1 getpversion $item $canc && d0=$R1
	Opts= OptS= Optp=1 getpversion $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 version anc2
	getanc2 $item +$nclone && a2=$R1
	for v in $a2
	do
		isanc2 $item $v $v2 && delanc2 $item +$nclone $v
	done
	addanc2 $item +$nclone $v2
	rm -f "$Job/$item/.dim/+$nclone/scan"	# Force index refresh
}

cmd merged '[-nqv] [V1]' 'Declare V1 merged in current clone' \
'	Declare V1 merged in current clone. This is useful when
	performing manual merging.'
merged()
{
	local item v1 v2 p2 a2 v

	getiv && item=$R1 v1=$R2
	isclone $v1 || die no clone
	getiv "$1" && v2=$R2
	[ "$v2" ] || die "invalid version $1"
	[ "$item" = "$R1" ] ||
	die "versions $item-$v1 and $R1-$v2 belong to different items"
	getpversion $item $v2 && p2=$R1
	# Avoid merging twice the same content
	[ $v1 = $v2 ] && nousage=1 die "cannot merge a clone into itself"
	isanc2 $item $v2 $v1 && {
		[ "$Optv" ] && echo "$p2 already merged" >&2
		return
	}
	# Keep the ancestor graph reduced: delete redundant version anc2
	getanc2 $item $v1 && a2=$R1
	for v in $a2
	do
		isanc2 $item $v $v2 && delanc2 $item $v1 $v
	done
	addanc2 $item $v1 $v2
	rm -f "$Job/$item/.dim/$v1/scan"	# Force index refresh
}

cmd unmerge '[-afnqv] [V1]' 'Undo a merge operation' \
'       Remove V1 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 v1 v pv
	getiv && item=$R1 nclone=${R2#+}
	isclone "$R2" || die no clone
	getanc2 $item +$nclone && a2=$R1 || die "nothing to unmerge"
	if [ "$1" ]
	then
		getiv "$1" && v1=$R2
		[ "$R1" = "$item" -a "$v1" ] || die "invalid version $1"
		lin "$a2" $v1 || die "invalid ancestor2 $v1"
	else
		[ "$Opta" ] ||
		case $a2 in (*\ *) die "cannot choose anc2" ;; esac
		v1=$a2
	fi
	for v in $v1
	do
		getpversion $item $v && pv=$R1
		[ "$Optv" ] && echo "removing anc2 $pv" >&2
		[ "$Optn" ] || delanc2 $item +$nclone $v
	done
	[ "$Optf" ] && copy
}

cmd clone '[-anv] [Item|V1]' 'Make current dir a clone of V1' \
'	Without argument, print current clone.
	With V1, make current directory a clone of V1.
	With Item, list all clones of Item.
	With -a, list all clones of the current job.'
clone()
{
	local item item1 version d clone nclone clonedir anc
	[ "$Jobdir" ] || nousage=1 die no job
	[ "$N1" ] && nousage=1 die "invalid flag -$N1"
	[ $# -gt 1 ] && die too many arguments
	if [ "$Opta" ]
	then
		# list all clones
		#
		[ "$1" ] && die too many arguments
		for d in $Job/*/*+*
		do
			[ -d "$d" ] || break
			getiv "${d##*/}" && item=$R1 version=$R2 clonedir=$R4
			[ "$clonedir" ] && echo $clonedir || die not a clone
		done
		return
	fi
	if [ $# = 0 ]
	then
		getiv && item=$R1 version=$R2 clonedir=$R4
		[ "$clonedir" ] && echo $clonedir || die not a clone
		return
	fi
	# Check version
	getiv "$1:" && item=$R1 version=$R2
	if [ ! "$version" ]
	then
		[ "$item" ] || nousage=1 die no item
		case $1 in
		(:|$item:) ;;
		(*)	nousage=1 die "invalid version $1" ;;
		esac
		# list all clones of item
		for d in $Job/$item/$item+*
		do
			[ -d "$d" ] || break
			getiv "${d##*/}" && clonedir=$R4
			[ "$clonedir" ] && echo $clonedir || die not a clone
		done
		return
	fi
	isclone $version && die "$item-$version is already a clone"
	case $PWD in
	($Jobdir/.dimlib|$Jobdir/.dimlib/*)
		nousage=1 die "cannot clone in $Jobdir/.dimlib" ;;
	esac
	# Check that current dir is not a clone
	getiv && item1=$R1 clone=$R2 clonedir=$R4
	[ "$clonedir" ] && nousage=1 die "$clonedir is already a clone"
	# Check that no subdir is a clone
	for d in $Job/*/*+*
	do
		[ -d "$d" ] || break
		getiv "${d##*/}" && clonedir=$R4
		case $clonedir in
		($PWD/*) nousage=1 die "$clonedir is already a clone" ;;
		esac
	done
	# Create a new clone
	nclone=1
	while [ -d "$Job/$item/.dim/+$nclone" ]
	do
		nclone=$(($nclone + 1))
	done
	clone=+$nclone
	[ "$Optv" ] &&
	echo "$item-$version is ancestor of $PWD ($item$clone)" >&2
	[ "$Optn" ] || {
		mkdir -p "$Job/$item/.dim/$clone" "$Job/$item/$item$clone"
		setattr anc $item $clone $version
		addattr desc $item $version $clone
		echo ".${PWD#$Jobdir}" >$Job/$item/.dim/$clone/path
		ln -s "../../../../..${PWD#$Jobdir}" \
			"$Job/$item/.dim/$clone/dir"
		set -- .* *
		echo "\$* $*" >&2
		if [ $# = 3 -a "$1" = . -a "$2" = .. -a "$3" = "*" ] ||
		   [ $# = 4 -a "$1" = . -a "$2" = .. -a "$3" = .dimlib -a $4 = "*" ]
		then
			Optv=0 copy	# populate empty clone
		else
			Optv=0 add .	# index non-empty clone
		fi
	}
	pversion $item $clone
}

# read $item/.dim/$version/path
# return R1=absolute_clonedir
getclonedir()
{
	local item=$1 version=$2

	getattr path $item $version || return 1
	case $R1 in
	(/*) return 0 ;;	# path used to be absolute
	esac
	R1=$Jobdir/${R1#./}
	R1=${R1#/.}
}

cmd reclone '[-nqv] V1' 'Change the ancestor of the current clone' \
'	Change the ancestor of the current clone, while preserving
	its content.'
reclone()
{
	local item item1 version anc clonedir
	# Check version
	if [ $_Cmd = reclone ]
	then
		getiv "$1:" && item=$R1 version=$R2
		[ "$version" ] && ! isclone $version || die "invalid version $1"
	else
		item=${1%%-*} version=${1#*-}
	fi

	# Check that we are a clone of the same item
	getiv && item1=$R1 clone=$R2 clonedir=$R4
	[ "$clonedir" ] || die not a clone
	[ "$item" = "$item1" ] || die "invalid item $item"

	# Update clone metadata
	[ "$Optv" ] &&
	echo "$item-$version is ancestor of $clonedir ($item$clone)" >&2
	[ "$Optn" ] && return
	getattr anc $item $clone && anc=$R1 || anc=0
	delattr desc $item $anc $clone
	setattr anc $item $clone $version
	delattr anc2 $item $clone $version  # anc cannot be anc2
	addattr desc $item $version $clone
	delattr desc2 $item $version $clone # anc cannot be anc2
	[ -f "$Job/$item/.dim/$version/scan" ] &&
	rm -f "$Job/$item/.dim/$version/scan"
	addanc2 $item $clone $anc
}

cmd unclone '[-nqv]' 'Remove current clone'
unclone()
{
	local item version
	[ $# -gt 0 ] && die too many arguments
	[ "$N1" ] && nousage=1 die "invalid flag -$N1"
	getiv && item=$R1 version=$R2
	isclone "$version" || die no clone
	unimport $version
}

cmd copy '[-fnqv]' 'Copy files from ancestor to clone'
copy()
{
	local item clone anc sum v1 oindex nindex curdir=$PWD
	[ "$1" ] && die too many arguments
	[ "$N1" ] && nousage=1 die "invalid flag -$N1"
	getiv $curdir && item=$R1 clone=$R2
	isclone "$clone" || die no clone
	getanc $item $clone && anc=$R1

	# Check that content is saved
	update_sum $item $clone && sum=$R1
	lget "$Job/$item/.sum/$sum" && v1=$R1 || [ "$Optf" ] ||
	nousage=1 die "current clone content not saved"
	[ "$v1" = "$anc" ] && return

	# Apply delta between ancestor and clone
	[ ${#Optv} -gt 1 ] && echo "delta from $item-$anc to $item$clone" >&2
	oindex=$Job/$item/.dim/$clone/index
	nindex=$Job/$item/.dim/$anc/index
	# copy from ancestor to clonedir
	delta "$oindex" "$nindex" "$Job/$item/$item-$anc" "$curdir"
	[ "$Optn" ] && return
	# link from clonedir to clone cache
	Cpio_link=l Optv= delta "$oindex" "$nindex" "$curdir" \
				 "$Job/$item/$item$clone"
	cp "$nindex" "$oindex"
	>|$Job/$item/.dim/$clone/scan
	getanc2 $item $clone && anc=$R1
	for anc in $anc
	do
		delanc2 $item $clone $anc
	done
}

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 no clone
	nlog=$Job/$item/.dim/+$nclone/log
	if [ -t 0 ]
	then
		${EDITOR:-vi} "$nlog"
	else
		cat >|$nlog
	fi
}

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

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

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

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

# Usage: isanc2 item anc2 desc2
# Return 0 if anc2 is a direct or indirect ancestor or ancestor2 of desc2
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 attr=$1 item=$2 more=$3 v2=$4 new list res next i j
	while [ "$more" ]
	do
		new=$more more=
		for i in $new
		do
			getattr $attr $item $i && list=$R1 || list=
			getattr ${attr}2 $item $i && list="$list $R1"
			[ "$v2" ] && lin "$list" $v2 && return 0
			for j in $list
			do
				lin "$res" $j || res="$res $j" more="$more $j"
			done
		done
	done
	[ "$v2" ] && 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
			lin "$b2" $v && res=$v && break
		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: getlocalanc item version
# Output: R1=list_of_local_ancestors
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
}

# Usage: plog item version
# 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
}

cmd log '[-ae] [V1 [V2]]' 'Print log between old V1 and new V2'
log()
{
	local d d1 a v item v1 v2
	getiv2 "$@" && item=$R1 v1=$R2 v2=$R3
	[ "$item" ] || nousage=1 die no item
	if [ "$Opte" ]
	then
		getiv "$1" && v1=$R2
		getlocalanc $item $v1
		for v in $R1 $v1
		do
			plog $item $v
		done
		return
	fi

	# Compute the segment: default is from old to new included
	# -e: old is the last exported ancestor (excluded)
	[ ! "$Opte" ] && d1=$(echo $v1; Opts=1 Opta=1 descendant $item-$v1) ||
	d1=$(Opts=1 Opta=1 descendant $(Opte=1 ancestor $item-$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
	[ "${rdir#$2}" = "$rdir" ] && rdir= || rdir=${rdir#$2}
	R1=${rdir:+${rdir#/}/}$rfile
}


cmd diff '[-cepSs] [-i File] [-o Opt] [V1 [V2]]' 'Print file differences' \
'	Print file differences between versions V1 and V2.'
diff()
{
	local item fp arg f1 f2 sum1 sum2 d1 d2 cd2 dr dp v1 v2 curdir=$PWD
	getiv2 "$@" && item=$R1 v1=$R2 v2=$R3 d1=$R4 d2=$R5

	# 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; } ||
	getattr sum $item $v1 && sum1=$R1
	f2=$Job/$item/.dim/$v2/index
	if isclone "$v2"
	then
		update_sum $item $v2 && sum2=$R1
		getclonedir $item $v2 && cd2=$R1
	else
		getattr sum $item $v2 && sum2=$R1
		cd2=$d2
	fi

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

	# 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= getpversion $item $v1 && iv1=$R1
			Opts= Optp= OptS= getpversion $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] [V1 [V2]]' 'Print line differences' \
'	Print line differences between versions V1 and V2.'
ldiff() { ddiff=1 diff "$@"; }

cmd wdiff '[-epNOSs] [-I File] [V1 [V2]]' 'Print word differences' \
'	Print word differences between versions V1 and V2.'
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 (! 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 (! Optn) print f | cp_cmd
			}
		for (f in c2)
			if (! (f in c1)) { 		# creation
				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: getnewversion item anc
# Output: R1=version
# 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 '--'.
getnewversion()
{
	local item=$1 anc=$2 eanc=${2%--*} loc=${2#*--} version num
	[ "$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
				version=$Branch
			else
				if [ "$Cmd" = export ]
				then
					case $Branch in
					(*[0-9]) ;;
					(*) Branch=${Branch}1 ;;
					esac
					version=$eanc-$Branch
				else
					[ "$loc" = "$anc" ] &&
					version=$eanc--$Branch ||
					version=$eanc--$loc-$Branch
				fi
			fi
		else
			if [ "$Cmd" = export ]
			then
				version=$eanc.1
			else
				[ "$loc" = "$anc" ] && loc=0
				version=$eanc--$loc.1
			fi
		fi
	else	# Do not force a new branch
		if [ "$OptM" ]	# Increment main ancestor last number
		then
			num=${eanc##*[!0-9]}
			version=${eanc%$num}$(($num + 1))
		elif [ "$loc" = "$anc" ]
		then
			if [ "$Cmd" = export ]
			then
				case $eanc in
				(*-$sname*[!0-9.]*)	# not our branch, fork
					version=$eanc-${sname}1
					;;
				(*-$sname*)		# stay on our branch
					num=${eanc##*[!0-9]}
					version=${eanc%$num}$(($num + 1))
					;;
				(*)			# not our branch, fork
					version=$eanc-${sname}1
					;;
				esac
			else
				version=$eanc--1
			fi
		else
			# Direct ancestor is a local version,
			# increment the version number
			case $loc in
			(*.*)	num=${loc%.*}.$((${loc##*.} + 1)) ;;
			(*)	num=$(($loc + 1)) ;;
			esac
			version=$eanc--$num
		fi
	fi

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

cmd save '[-nvqb] [-B Version] [V1]' 'Save clone V1 to a new local version' \
'	Save the content of clone V1 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 clonedir
	getiv && item=$R1 nclone=${R2#+}
	isclone "$R2" || die no clone
	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
	getattr anc $item +$nclone && anc=$R1
	getnewversion $item $anc && vers=$R1
	[ "$Optv" ] && echo "saving $item-$vers" >&2
	[ "$Optn" ] || mkdir "$Job/$item/.dim/$vers"

	# Read and duplicate ancestor (meta)data
	index=$Job/$item/.dim/$vers/index
	sum=$Job/$item/.dim/$vers/sum
	getattr sum $item $anc && 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
	getclonedir $item +$nclone && clonedir=$R1
	delta "$Job/$item/.dim/$anc/index" "$nindex" "$clonedir" \
	      "$Job/$item/$item-$vers"
	[ "$Optn" ] && {
		reclone $item-$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 "$clonedir"

	# 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
		unimport $vers
		pversion $item $overs
	}

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

	# Rewire the clone: new version becomes its ancestor
	reclone $item-$vers

	# Update anc2 and corresponding desc2
	getattr anc2 $item +$nclone && lanc2=$R1
	for v in $lanc2
	do
		delattr desc2 $item $v +$nclone
		addattr desc2 $item $v $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 dir
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" | urlencode)
		;;
	(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/dim_bigurl.$$
				cd /tmp/dim_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
	get_filelist "$1" "$2" >|/tmp/dim_filelist.$$
	while read f
	do
		[ "$f" = @error ] && nousage=1 die "get_filelist failed"
		[ "$f" = "*" ] && continue
		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 ;;
		(file:/*) cp "${1#file:}/$2/$file" "$file" ;;
		esac
		gsize=$((0 + $(wc -c <$file)))
		[ ! "$size" -o "$gsize" = "$size" ] || {
				error "wrong size of $file: $gsize"
				rm "$file"
		}
	done </tmp/dim_filelist.$$
	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 lib server'
get()
{
	local user itemlist item
	[ $# -gt 1 ] && die too many arguments

	[ "$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" ]
	then
		[ "$1" ] && die too many arguments
		get_filelist "$Url" item >|/tmp/dim_items.$$
		while read item
		do
			[ "$item" = "*" ] && break
			[ "$item" = @error ] && die
			itemlist="$itemlist $item"
		done </tmp/dim_items.$$
		set -- $itemlist
	else
		getiv "$1:" && item=$R1
		set -- $item
	fi
	for item
	do
		[ -d "$Archive/$item" ] || {
			mkdir -p "$Archive/$item" || die
			[ "$Optn" ] && trap "rmdir \"$Archive/$item\"" EXIT
		}
		get_dir "$Url" "archive/$item"
	done
}

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

	if [ $# -gt 0 ] || [ "$item" -a ! "$Opta" ]
	then
		put_dir "$Url" "archive/${1:-$item}" ${1:-$item}
		return
	fi
	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 '[-v0] Url' 'Send a HTTP request and print data.'
wget()
{
	local nredir=0 i url
	Http_redirect_url=$1
	[ "$1" ] || { error "wget(): missing argument"; return 1; }
	while [ "$Http_redirect_url" -a $nredir -le 5 ]
	do
		[ $nredir -gt 0 ] && Connected_host= Connected_port=
		nredir=$(($nredir + 1))
		url=$Http_redirect_url
		http_fill_request "$Http_redirect_url"
		http_connect || die "could not connect to $Host $Port"
		[ ${#Optv} -gt 1 ] && printf %s "## send to $Host:$Port #{
$http_req" >&2
		{
			printf %s "$http_req"
			[ -f "$Http_file" ] && ncat <$Http_file
			printf %s "$http_endreq"
			[ ${#Optv} -gt 1 ] && printf %s "$http_endreq" >&2
		} >&3
		[ ${#Optv} -gt 1 ] && echo "}#" >&2
		http_response <&4 || {
			[ ${#Optv} -gt 1 ] && echo "## retry $url" >&2
			Http_redirect_url=$url
		}
	done
	[ $nredir -le 5 ] || error too many redirections
}

http_connect()
{
	local nhost=$Host nport=$Port tcp_pref
	[ "$Host" ] || die "http_connect(): missing host"
	[ "$Port" ] || die "http_connect(): missing port"
	[ "$http_proxy" ] && nhost=${http_proxy%:*} nport=${http_proxy#*:}
	[ "$Connected_host" = "$nhost" ] &&
	[ "$Connected_port" = "$nport" ] && {
		[ ${#Optv} -gt 1 ] &&
		echo "## already connected to $nhost:$nport" >&2
		return 0
	}
	[ ${#Optv} -gt 1 ] && echo "## connect to $nhost:$nport" >&2
	# Check for /dev/tcp on BSD portals
	case $Sys in
	(*BSD) tcp_pref=$(/sbin/mount | awk '$5 == "portal" {print $3}') ;;
	esac
	# If not found, check for shell providing /dev/tcp (ksh93, bash)
	[ ! "$tcp_pref" ] && [ "$Valid_dev_tcp" ] && tcp_pref=/dev

	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 : PIPE		 # Do not abort shell if nc exits.
		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=chunked		# HTTP defaults to chunked data
	local line l2 http_line tmp=/tmp/http_response.$$
	local eof=1
	Http_redirect_url=
	while read -r line
	do
		[ "$eof" ] && eof= && [ ${#Optv} -gt 1 ] &&
		echo "## receive from $Connected_host:$Connected_port #{" >&2
		[ ${#Optv} -gt 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
			 out=5 ;;
		(HTTP*[1345][0-9][0-9]*)	# output is stderr
			error $line; len=close out=2 ;;
		(Connection:\ [Cc]lose)
			len=close ;;
		(Content-Length:*)		# read $len bytes
			[ $len != close ] && len=${line#*: } ;;
		(Content-Type:*text/*)
			datatype=text ;;
		(Location:*)
			Http_redirect_url=${line#*: } ;;
		(*:*)
			;;
		(*)
			[ "$http_line" ] || continue	# unexpected newline
			case $len in
			(chunked)
				[ "$out" = 2 ] && break
				while read line
				do
					# line: data length in hexa
					len=$(printf %d 0x${line%})
					[ "$len" = 0 ] && break
					ncat $len
					read line
				done >&$out ;;
			(close)
				ncat >&$out ;;
			([0-9]*)
				ncat $len >&$out ;;
			esac
			[ "$Http_proto" = 0 ] && {
				[ ${#Optv} -gt 1 ] &&
				echo "}# close $Host:$Port" >&2
				Connected_host= Connected_port=
			}
			break ;;
		esac
	done
	[ "$eof" ] || return 0
	# Unexpected connection close, possible in HTTP/1.1
	[ ${#Optv} -gt 1 ] &&
	echo "## unexpected close of $Connected_host:$Connected_port" >&2
	Connected_host= Connected_port=
	return 1
}

# Usage: ncat [len]
# Read the specified number of bytes from stdin and print them to stdout
# If no len specified, read until EOF
# In debug mode (-vv), duplicate data to stderr
ncat()
{
	local bs=4096 len=$1 	# bs is the default block size
	local nil n tmp=/tmp/dim_ncat.$$
	[ -f "$tmp" ] || trap "rm -f $tmp" EXIT
	if [ ! "$len" ]
	then
		if [ ${#Optv} -gt 2 ]
		then
			tee $tmp
			cat -v <$tmp >&2
		else
			cat
		fi
		return
	fi
	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 $tmp
		[ ${#Optv} -gt 2 ] && cat -v <$tmp >&2
		n=$(wc -c <$tmp)
		len=$(($len - $n))
	done
}

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 version
# 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_version item version
# FIXME: accept path as argument
# FIXME: add option to import directly in clone dir, bypassing job
# Current dir must be Tmpdir
import_version()
{
	local item=$1 version=$2 arfile ranc inflate
	local extract f a2 la2 a d hist hist2
	local s1 s2 file cfile anc sum ldesc nindex aindex mod d noanc
	[ "$version" = 0 ] && { mkitem $item; return 0; }

	[ -d "$Job/$item/.dim/$version" -o -d "$Tmpdir/$version" ] && return
	[ -f "$Job/$item/.removed/$version" -a ! "$Optf" ] && {
		[ "$Optv" ] && echo "Skip removed version: $item-$version" >&2
		return
	}
	get_archive $item "$version" && arfile=$R1 ranc=$R2 || return
	[ "$ranc" -a ! "$Optn" ] && {
		import_version $item $ranc ||
		nousage=1 die "import failed: $item-$version"
	}
	# Find out compression 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" ] && { echo $item-$version; return; }
	rm -fr cat ned
	$inflate $arfile | $extract

	if [ -d ned ]
	then
		# For each delta, copy corresponding ancestor file into cat/
		if [ -d "$Job/$item/.dim/$ranc" ]
		then
			[ "$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 "$Tmpdir"
			}
		else
			>cat/meta/index
		fi

		# 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" ] || error "wrong index: $item-$version"

		# 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
	else
		# archive is absolute
		[ -d "$Job/$item/.dim" ] || mkitem $item
		lget "$Tmpdir/cat/meta/hist" && hist=$R1
		lget "$Tmpdir/cat/meta/hist2" && hist2=$R1
		for a in $hist
		do	 # Adopt the closest existing ancestor in history
			[ -d "$Job/$item/.dim/$a" ] &&
			echo "$a" >|$Tmpdir/cat/meta/anc && break
		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"
	getattr desc $item $anc && ldesc=$R1 || ldesc=
	nindex=$Tmpdir/cat/meta/index
	aindex=$Job/$item/.dim/$anc/index
	mkdir -p "$Job/$item/.dim/$version"
	cd cat/meta
	ln * "$Job/$item/.dim/$version"
	lnd "$Job/$item/$item-$anc" "$Job/$item/$item-$version" \
	    "$aindex"
	[ ${#Optv} -gt 1 ] && echo "delta from $Tmpdir to $item-$version" >&2
	Cpio_link=l delta "$aindex" "$nindex" "$Tmpdir/cat/data" \
			   "$Job/$item/$item-$version"

	# Allocate the version
	[ -f "$Job/$item/.sum/$sum" ] || echo "$version" >$Job/$item/.sum/$sum
	[ -f "$Job/$item/.version/$version" ] ||
	ln "$Job/$item/.dim/$version/sum" "$Job/$item/.version/$version"

	# Check if the version is a parent of already existing versions
	# in the history of ancestor's descendants, and restore parentship.
	for d in $ldesc
	do
		gethist2 $item $d && hist2=$R1
		lin "$hist2" $version && addanc2 $item $d $version
		gethist $item $d && hist=$R1
		lin "$hist" $version || continue
		# $version becomes parent of $d
		delattr anc2 $item $d $version
		delattr desc2 $item $version $d
		delattr desc $item $anc $d
		addattr desc $item $version $d
		setattr anc $item $d $version
	done

	# Insert the version in the item ancestor tree
	addattr desc $item $anc $version
	getattr anc2 $item $version && lanc2=$R1 &&
	for a in $lanc2
	do
		isexp $a || continue
		[ -d "$Job/$item/.dim/$a" ] || continue
		# keep the anc2 graph reduced
		for d in $ldesc
		do
			delattr desc2 $item $a $d
			delattr anc2 $item $d $a
		done
		[ -d "$Job/$item/.dim/$a" ] &&
		addattr desc2 $item $a $version
	done
	rm -f "$Job/$item/.removed/$version"
	cd $Tmpdir
	[ "$Optv" ] && echo $item-$version || true
}

cd_import_tmpdir()
{
	Tmpdir=${Job%/*}/import.$$
	mkdir -p "$Tmpdir"
	cd $Tmpdir || die "cannot chdir to $Tmpdir"
	trap "cd /; rm -fr \"$Tmpdir\"" EXIT
}

import_item()
{
	local item ivers i v curdir=$PWD
	if [ "$1" ]
	then
		i=${1%%-*}
		item=$i
		ivers=${1#$item-}
		[ "$ivers" = "$item" ] && ivers=
	fi
	[ "$item" ] || nousage=1 die no item
	[ -d "$Archive/$item" ] || die "no item: $item"
	[ -d "$Job/$item" ] || {
		mkdir -p "$Job/$item" || die
		[ "$Optn" ] && trap "rmdir \"$Job/$item\"" EXIT
	}
	cd_import_tmpdir
	for v in $ivers "$Archive"/$item/$item-*--*.*.*
	do
		v=${v##*/}; [ "$v" = "$item-*--*.*.*" ] && continue
		v=${v#$item-}; v=${v%%--*.*.*}
		import_version $item $v
		[ "$ivers" ] && { cd "$curdir"; return; }
	done
	for v in "$Archive"/$item/$item-*.*.*
	do
		case $v in (*--*) continue ;; esac
		v=${v##*/}; [ "$v" = "$item-*.*.*" ] && continue
		v=${v#$item-}; v=${v%.*.*}
		import_version $item $v
	done
	cd "$curdir"
}

cmd import '[-anqv] [Item|V1]' 'Extract archives into versions'
import()
{
	local item=$1 version
	[ $# -gt 1 ] && die too many arguments
	[ "$Jobdir" ] || return
	if [ "$Opta" ]
	then
		[ "$item" ] && die too many arguments
		for item in "$Archive"/*
		do
			[ -d "$item" ] && (import_item ${item##*/})
		done
		return
	fi
	getiv $item: && item=$R1
	[ "$item" ] || die missing argument
	[ -d "$Archive/$item" ] || die invalid item "$item"

	version=${1#$item-}
	if [ "$version" != "$item" ]
	then
		# If a specific version is given as argument, import it only
		[ -d "$Job/$item" ] || mkdir -p "$Job/$item"
		( cd_import_tmpdir; import_version $item $version)
	else
		(import_item $item)
	fi
}

# FIXME: propagate the log to descendants
cmd unimport '[-nvq] V1' 'Undo import of V1' \
'	Undo import of version V1 in the current job.
	Archives and clone data are not deleted.'
unimport()
{
	local item version clonedir anc a d desc desc2 hist hist2 sum
	[ $# = 0 ] && die missing version
	[ $# -gt 1 ] && die too many arguments
	getiv "$1" && item=$R1 version=$R2 clonedir=$R4
	[ "$version" -a "$version" != 0 ] || die "invalid version $1"
	if [ "$Optv" ]
	then
		[ "$clonedir" ] &&
		echo "removing $clonedir ($item$version)" >&2 ||
		echo "removing $Job/$item/$item-$version" >&2
	fi
	[ "$Optn" ] && return
	getattr anc $item $version && anc=$R1
	getattr anc2 $item $version && anc2=$R1
	getattr desc $item $version && desc=$R1
	getattr desc2 $item $version && desc2=$R1
	delattr desc $item $anc $version
	for a in $anc2
	do
		delattr desc2 $item $a $version
	done
	if [ "$clonedir" ]
	then
		rm -fr "$Job/$item/$item$version" "$Job/$item/.dim/$version"
		return
	fi
	# Remove a version already saved or exported
	getattr sum $item $version && sum=$R1 && rm -f "$Job/$item/.sum/$sum"
	[ -d "$Job/$item/.removed" ] || mkdir "$Job/$item/.removed"
	if isexp $version
	then
		>|$Job/$item/.removed/$version
		gethist $item $version && hist="$version $R1"
		gethist2 $item $version && hist2="$R1"
		rm -fr "$Job/$item/$item-$version" "$Job/$item/.dim/$version"
	else
		mv "$Job/$item/.dim/$version" "$Job/$item/.removed/$version"
		mv "$Job/$item/$item-$version" \
		   "$Job/$item/.removed/$version/data"
	fi
	for d in $desc
	do
		# reparent
		setattr anc $item $d $anc
		addattr desc $item $anc $d
		for a in $anc2
		do
			addanc2 $item $d $a
		done
		# Save history of removed version in descendants
		[ "$hist" ] && setattr hist $item $d "$hist"
		[ "$hist2" ] && setattr hist2 $item $d "$hist2"
	done
	for d in $desc2
	do
		# reparent
		addanc2 $item $d $anc
		for a in $anc2
		do
			addanc2 $item $d $a
		done
	done
}

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

cmd item '[-a]' 'Print the current item'
item()
{
	local item
	[ "$1" ] && die too many arguments
	[ "$Jobdir" ] || return
	if [ "$Opta" ]
	then
		for item in "$Archive"/*
		do
			[ -d "$item" ] && echo "${item##*/}"
		done
	else
		getiv && item=$R1
		[ "$item" ] && echo "$item"
	fi
}

cmd job '[-lRq]' 'Print the current job' \
'	Print the job containing the current directory.
	With -l, print also the Url of the library serving the job.
	With -R, recursively search sub-directories for jobs.
	With -q, return only the first job found.'
job()
{
	local d libtxt
	[ "$1" ] && die too many arguments
	[ "$N1" ] && nousage=1 die "invalid flag -$N1"
	if [ "$Jobdir" ]
	then
		[ "$Optl" ] && echo "$Jobdir served by $Url" || echo "$Jobdir"
		return
	fi
	if [ "$Optq" ]
	then
		for d in */.dimlib */*/.dimlib
		do
			[ -f "$d/url" ] && break
		done
		[ -f "$d/url" ] ||
		d=$(find * -type d -name .dimlib -prune 2>|/dev/null |
			 head -1)
		[ -f "$d/url" ] || return
		[ "$Optl" ] && libtxt=" served by $(cat "$d/url")"
		echo "$PWD/${d%/*}$libtxt"
		return
	fi
	if [ "$OptR" ]
	then
		find * -type d -name .dimlib -prune 2>|/dev/null |
		while read d
		do
			[ "$Optl" ] && libtxt="served by $(cat $d/url)"
			echo "$PWD/${d%/*}$libtxt"
		done
	fi
}

cmd lib '' 'Print the Url of the current library' \
'	Print the Url of the library serving the current job.'
lib()
{
	[ "$1" ] && die too many arguments
	[ "$N1" ] && nousage=1 die "invalid flag -$N1"
	[ "$Jobdir" ] || return
	echo $Url
}

cmd version '[-apsS] [V1|Dir]' 'Print item version info'
version()
{
	local item version
	[ $# -gt 1 ] && die too many arguments
	getiv "$1" && item=$R1 version=$R2
	[ "$item" ] || nousage=1 die no item
	if [ "$Opta" ]
	then
		for version in $Job/$item/.dim/*
		do
			case ${version##*/} in
			('*'|.|..)	continue ;;
			(+*|*--*)	[ "$Opte" ] && continue ;;
			esac
			pversion $item ${version##*/}
		done
		return
	fi
	[ "$version" ] || die "invalid version ${1:-.}"
	pversion $item $version
}

cmd tarball '[-nqv] V1' 'Generate a gzipped tar file of V1'
tarball()
{
	local item vers d pvers curdir=$PWD
	getiv "$1" && item=$R1 vers=$R2
	[ "$vers" ] || die "invalid version ${1:-.}"
	Opts= OptS= Optp= getpversion $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"
}

cmd client '[-ap]' 'List client of current clone'
client()
{
	local i client o max list supplier curdir=$PWD
	[ $# = 0 ] || die too many arguments
	getiv && supplier=$R4
	[ "$supplier" ] || die no clone
	cd "$Job"
	if [ "$Opta" ]
	then
		for i in */.dim/+*
		do
			item=${i%%/*} version=${i##*/}
			getclonedir $item $version && client=$R1 || return
			case $supplier in
			($client/*)
				echo "${Optp:+$Job/}$item$version" ;;
			esac
		done
	else
		o=$IFS max=0
		for i in */.dim/+*
		do
			item=${i%%/*} version=${i##*/}
			getclonedir $item $version && client=$R1 || return
			case $supplier in
			($client/*)
				IFS=/; set -- $client; IFS=$o
				if [ $# -gt $max ]
				then
					max=$# list=$item$version
				elif [ $# = $max ]
				then
					list="$list $item$version"
				fi
				;;
			esac
		done
		[ $max != 0 ] &&
		for i in $list
		do
			echo "${Optp:+$Job/}$i"
		done
	fi
	cd "$curdir"
}

cmd supplier '[-ap] [V1]' 'List suppliers of current clone'
supplier()
{
	local i supplier client o min list curdir=$PWD
	[ $# = 0 ] || die too many arguments
	getiv && client=$R4
	[ "$client" ] || die no clone
	cd "$Job"
	if [ "$Opta" ]
	then
		for i in */.dim/+*
		do
			item=${i%%/*} version=${i##*/}
			getclonedir $item $version && supplier=$R1 || return
			case $supplier in
			($client/*)
				echo "${Optp:+$Job/}$item$version" ;;
			esac
		done
	else
		o=$IFS min=9999
		for i in */.dim/+*
		do
			item=${i%%/*} version=${i##*/}
			getclonedir $item $version && supplier=$R1 || return
			case $supplier in
			($client/*)
				IFS=/; set -- $supplier; IFS=$o
				if [ $# -lt $min ]
				then
					min=$# list=$item$version
				elif [ $# = $min ]
				then
					list="$list $item$version"
				fi
				;;
			esac
		done
		[ $min != 9999 ] &&
		for i in $list
		do
			echo "${Optp:+$Job/}$i"
		done
	fi
	cd "$curdir"
}

cmd list '[-q][V1|File]' 'Print version of objects' \
'	Print version of objects.
	If object is an item version V1, for each file in V1,
	print the last version of item where file was modified.
	If object is a file File, for each line of File, print
	the last version of item where line was modified.'
list()
{
	local item vers ancs sum last a rancs pa
	[ -f "$1" ] && { label "$1"; return; }
	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
	getpversion $item $vers && last=$R1

	if [ "$Optq" ]
	then
		awk '{print substr($0, 44)}' <$Job/$item/.dim/$vers/index
		return
	fi

	# Revert ancestor list in rancs
	for a in $ancs; do rancs=$a" "$rancs; done
	for a in $rancs $vers
	do
		getpversion $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}
	'
}

# Usage: label file
# Print version of last change per line
label()
{
	local file item vers rdir rfile ancs max n node a ad af
	case $1 in
	(./*|../*|/*) 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 getpversion $item $vers && rdir=$R1
	relpath "$file" "$rdir" && rfile=$R1
	istextfile "$file" || die "binary file: $file"
	Opta=1 getanc $item $vers && ancs=$R1
	getpversion $item $vers && node=$R1
	max=$N1 n=0
	for a in $ancs
	do
		echo "node $node"
		getpversion $item $a && node=$R1
		Opts= OptS= Optp=1 getpversion $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 "$Cmdlist" $1 && { R1=$1; return 0; }
	R1= ambig=0
	for c in $Cmdlist
	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 $Shellversion in (bash*|ksh93*|zsh*) nind=1 ;; esac
	while getopts :0123456789aAB:bcC:D:efHi:IlMnNOo:pqRsSU:Vvw opt
	do
		case $opt in
		([0-9])
			case $Shellversion 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
			[ $opt = 0 ] && Http_proto=0
			;;
		([aAbcefHIlMnNOpRSsw]) 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)	Optq=1 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) $Shellversion"
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()
Connected_host= Connected_port= Http_proto=1

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 "$@"
rm -rf /tmp/dim_*.$$
