summaryrefslogtreecommitdiff
path: root/bin/dv
diff options
context:
space:
mode:
Diffstat (limited to 'bin/dv')
-rwxr-xr-xbin/dv556
1 files changed, 556 insertions, 0 deletions
diff --git a/bin/dv b/bin/dv
new file mode 100755
index 0000000..0737295
--- /dev/null
+++ b/bin/dv
@@ -0,0 +1,556 @@
+#!/bin/sh
+
+# Local dv (aka sim)
+
+dv_version='dv-0.23 Copyright 2013-2014 Marc Vertes, Philippe Bergheaud'
+unset CDPATH
+export LC_ALL=C IFS='
+'
+
+anc() {
+ usage 'anc [Version]' 'print ancestor' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD)
+ [ "$Opt_N" ] && Opt_N=$(($Opt_N - 1))
+ [ "$Opt_N" ] && info anc $R2 $Opt_N || echo $R2
+ ;;
+ (*) info anc $1 ${Opt_N:-1};;
+ esac
+}
+
+cache() {
+ isremote "$1" || return
+ o=$1 _d=$cacheprefix/$1; pd=${_d%/*}; ppd=${pd%/*} t=$pd/.${_d##*/}
+ [ -d "$ppd" ] || mkdir -p "$ppd"
+ [ -L "$ppd/.dv." ] || ln -s . "$ppd/.dv."
+ [ -f "$_d/.dv" ] && return
+ [ -d "$pd" ] || mkdir -p "$pd"
+ set -- $pd/*; shift $(($# - 1))
+ [ -d "$1" ] && ropt="--link-dest=../${1##*/}" || ropt=
+ rsync -aDS $ropt "$o/" "$t" && mv "$t" "$_d"
+}
+
+client() {
+ usage 'client [Version]' 'print clients' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD)
+ while getpdir .dv ..
+ do
+ cd "$R1"
+ getdv ".dv"
+ [ "$OptM" ] && echo "$R2 -> $PWD" || echo "$R2"
+ ((Opt_N = Opt_N-1)) || break
+ done ;;
+ (*) info cli $1 ${Opt_N:-9999} | sort -u ;;
+ esac
+}
+
+clone() {
+ usage 'clone [-A Author] [-m Msg] Version [Dir]' \
+ 'Copy version to dir' && return
+ [ "$1" ] || die missing argument
+ [ "$3" ] && die too many arguments
+ getpdir .dv . && root=$R1 && getdv $root/.dv && lib=$R1 item=${R2%/*}
+ new_version=${1##*/}
+ if [ "$new_version" = "$1" ]
+ then
+ getdv .dv && lib=$R1 new_item=${R2%/*} || die no item
+ else
+ v=${1%/$new_version}
+ new_item=${v##*/}
+ [ "$new_item" = "$v" ] || new_lib=${v%/$new_item}
+ fi
+ [ "$PWD" = "$root" ] && [ "$item" != "$new_item" ] &&
+ die "already a clone of $item"
+ lib=${lib:-$new_lib}
+ [ "$lib" ] || die no lib
+ [ "$new_lib" -a "$new_lib" != "$lib" ] && die "already in lib: $lib"
+ if [ "$new_version" = 0 ] # Init item in lib
+ then
+ initlib "$lib"
+ getdvinfo "$lib" || die "could not lock $lib"
+ mkdir -p /tmp/dv.$$/$new_item/0
+ dv_write $new_item/0 $new_item/0 >/tmp/dv.$$/$new_item/0/.dv
+ { cat /tmp/dv.$$/$new_item/0/.dv; echo; } >>/tmp/dv.$$/.dvinfo.0
+ ln -f /tmp/dv.$$/.dvinfo.0 /tmp/dv.$$/.dvinfo.1
+ rsync -a /tmp/dv.$$/ "$lib"
+ fi
+ if [ ! "$2" ] # Use the item name as default directory (a la git)
+ then
+ set -- "$1" "${1%/*}"
+ set -- "$1" "${2##*/}"
+ echo cloning into "$2"
+ fi
+ [ ! -d "$2" ] && mkdir "$2"
+ cd "$2" || die cannot chdir to "$2"
+ # Rename existing supplier directories to allow supplier symlinks
+ find . -name .dv | while read l
+ do
+ l=${l%/.dv} && [ "$l" = "." ] && continue
+ mv "$l" "$l.dvold"
+ done
+ # Sync from lib, starting from most client item, up to last supplier
+ d=. s=$new_item/$new_version
+ while [ "$d" ]
+ do
+ [ -L "$d" ] && {
+ rm -f "$d"
+ [ -d "$d.dvold" ] && mv "$d.dvold" "$d"
+ }
+ cache "$lib/$s" && pref=$cacheprefix/ || pref=
+ rsync -aDS "$pref$lib/$s/" "$d"
+ dv_header "$d" Anc "$s"
+ dv_header "$d" Lib "$lib"
+ # Find next symlink pointing to a supplier
+ d= l=
+ eval "$(find . -type l | while read -r l
+ do
+ f=$(readlink "$l")
+ case $f in
+ (*/.dv./*) echo "d=\"$l\" s=\"${f#*/.dv./}\""
+ break;;
+ esac
+ done)"
+ done
+}
+
+desc() {
+ usage 'desc [Version]' 'print descendants' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD) ;;
+ (*) info desc $1 ;;
+ esac
+}
+
+die() { echo "$0: fatal: $@" >&2; exit 1; }
+
+diff() {
+ usage 'diff [-x pat] [-F File] [V1 [V2]]' 'Print differences' && return
+ getpdir .dv && getdv "$R1/.dv" || die not in a clone
+ lib=$R1 item=${R2%/*} old=${R2#*/}
+ [ "$lib" = "" ] && [ -L "../../.dv." ] && lib=${PWD%/*/*}
+ if [ "$2" ]
+ then
+ case $2 in
+ (.) new=. new_is_clone=1 ;;
+ (*) new=$lib/$item/$2 ;;
+ esac
+ cache "$new" && np=$cacheprefix/ || np=
+ old=$lib/$item/$1
+ elif [ "$1" ]
+ then
+ case $1 in
+ (.) new=. new_is_clone=1 ;;
+ (*) new=$lib/$item/$1 ;;
+ esac
+ cache "$new" && np=$cacheprefix/ || np=
+ getdv "$np$new/.dv" && old=${R1:-$lib}/$R2
+ else
+ new=$PWD new_is_clone=1
+ cache "$new" && np=$cacheprefix/ || np=
+ getdv "$np$new/.dv" && old=${R1:-$lib}/$R2
+ fi
+ cache "$old" && op=$cacheprefix/ || op=
+ [ "$Opts" ] || printf "old %s\nnew %s\n" "$old" "$new"
+ # Itemized diff for all suppliers, deepest first
+ cd "$np$new" && for f in $(find . -type f -name .dv | sort -r)
+ do
+ d=${f%/.dv}
+ [ "$d" = . ] && prefix= || prefix=${d#./}/
+ getdv "$f" && cache "$lib/$R2"
+ [ "$new_is_clone" ] && dest=$np$d || dest=$prefix$d
+ diffdir "$op$lib/$R2" "$dest"
+ Optx="$Optx --exclude=${d##*/}"
+ done
+}
+
+# Usage: diffdir oldpath newpath
+# Print differences between 2 directories
+diffdir() {
+ [ -f "$2/.dvignore" ] && xf=--exclude-from=$2/.dvignore || xf=
+ rsync -aDSniv $xf --delete --exclude=".dv.*/" $Optx "$2/" "$1" |
+ awk -v OptF="${OptF#./}" -v prefix=$prefix '
+ NF == 0 {exit}
+ NR < 2 || /\/*\.dv$/ || /\/$/ {next}
+ # Match an itemized status for all versions of rsync -i
+ $1 !~ /^[<>ch.*][fdLDS+][.+?cstpoguaxz]+$/ {next}
+ {key = $1; file = substr($0, length(key) + 2)}
+ OptF && OptF != file {next}
+ key == "*deleting" {print "deleted " prefix file; next}
+ substr(key, 3, 7) == "+++++++" {print "created " prefix file; next}
+ { # Avoid false positive if only mtime is changed.
+ of = "'$1'/" file; gsub("'\''", "'\'\\\\\'\''", of)
+ nf = "'$2'/" file; gsub("'\''", "'\'\\\\\'\''", nf)
+ if (substr(key, 2, 1) == "L") { # Symlink
+ src = target = file
+ sub(/.* -> /, "", target);
+ sub(/ -> .*/, "", src);
+ "readlink '$2'/" src | getline otarget
+ if (target != otarget)
+ print "changed " src
+ } else if (system("cmp -s '\''" of "'\'\ \''" nf "'\''"))
+ print "changed " file
+ }'
+ # Or: scan key for file/link size, checksum or permission change
+ # rsync -c is required
+ #key ~ /[cps]/ { print "changed " prefix file }'
+}
+
+dv_write() {
+ cat <<- EOT
+ From $(username)
+ Date: $(date +"%F %T %z")
+ Version: $1
+ Anc: $2
+ EOT
+
+ [ "$OptA" ] && echo "Author: $OptA"
+ printf "\n%s\n" "$Optm"
+}
+
+dv_header() {
+ awk -v val="$3" '/^'$2':/ {print "'$2': " val; done = 1; next}
+ NF == 0 && done == 0 {print "'$2': " val; done = 1}
+ {print}' $1/.dv >$1/.dv.$$ && mv $1/.dv.$$ $1/.dv
+}
+
+# Usage: getdv path
+# Return lib in R1, anc in R2, version in R3
+getdv() {
+ R1= R2=
+ while read -r line
+ do
+ case $line in
+ ("Lib: "*) R1=${line#Lib: };;
+ ("Anc: "*) R2=${line#Anc: };;
+ ("Version: "*) R3=${line#Version: };;
+ esac
+ done <$1
+ [ "$R1" -o "$R2" ]
+}
+
+# Usage: getdvinfo lib
+# Get exclusive write access to a dvlib, for new version commit
+getdvinfo() {
+ [ -d /tmp/dv.$$ ] || mkdir /tmp/dv.$$
+ [ "$Optn" ] && { rsync "$1/.dvinfo.1" /tmp/dv.$$/.dvinfo.0; return; }
+ t=0
+ for i in 1 2 3 4 5
+ do
+ rsync --remove-source-files "$1/.dvinfo.0" /tmp/dv.$$/ && break
+ t=$(($t + $i)) && sleep $t
+ done
+ test -f /tmp/dv.$$/.dvinfo.0
+}
+
+putdvinfo() {
+ [ "$Optn" ] && return
+ ln -f /tmp/dv.$$/.dvinfo.0 /tmp/dv.$$/.dvinfo.1
+ rsync -a /tmp/dv.$$/.dvinfo.[01] "$1/"
+ isremote "$1" && cp /tmp/dv.$$/.dvinfo.0 "$cacheprefix/$1/"
+}
+
+# Usage: getpdir file [path]
+# Return absolute parent dir of path or PWD containing file
+getpdir() {
+ R1="$([ -d "${2:-.}" ] && cd "${2:-.}" && pwd)"
+ while [ "$R1" ]
+ do
+ [ -f "$R1/$1" ] && return || R1=${R1%/*}
+ done
+ return 1
+}
+
+# Usage: getprd file [path]
+# Return relative path to parent directory containing file
+getprd() {
+ R2="$(cd "${2:-.}" && pwd)"; R2=${R2%/*} R1=..
+ while [ "$R2" ]
+ do
+ [ -f "$R2/$1" ] && return || R2=${R2%/*} R1=$R1/..
+ done
+ return 1
+}
+
+info() {
+ usage info && return
+ case $2 in
+ (*:*) lib=${2%/*/*} ;;
+ (*) getpdir .dv && getdv $R1/.dv && lib=$R1 || die no dvlib found
+ esac
+ case $2 in # $2 specifies:
+ (*/*/*) v=${2%/*/*}; v=${2#$v/} ;; # lib/item/version
+ (*/*) v=$2 ;; # item/version
+ (*) v=${R2%/*}/$2 ;; # version
+ esac
+ isremote "$lib" && {
+ p=$cacheprefix
+ [ -d "$p/$lib" ] || mkdir -p "$p/$lib"
+ [ "$Optl" ] || rsync -a "$lib/.dvinfo.1" "$p/$lib/"
+ } || p=
+ info_query $1 $v $3 <$p/$lib/.dvinfo.1
+}
+
+info_query()
+{
+ awk -v arg0=$1 -v arg1=$2 -v arg2=$3 '
+ /^From / {
+ if (v) msg[v] = var["Body"]
+ delete var
+ var["From"] = substr($0, 6)
+ header = 1
+ next
+ }
+ header == 1 && NF == 0 {
+ v = var["Version"]; a = var["Anc"]; s = var["Sup"]
+ if (a != v) {
+ anc[v] = a; desc[a] = desc[a] ? desc[a] " " v : v
+ }
+ n = split(s, as)
+ for (i = 1; i <= n; i++) {
+ cli[as[i]] = cli[as[i]] ? cli[as[i]] " " v : v
+ }
+ sup[v] = s; msg[v] = var["Msg"]; date[v] = var["Date"]
+ if (var["Root"]) root[v] = var["Root"]
+ header = 0
+ next
+ }
+ header == 1 {
+ i = index($0, ":")
+ var[substr($0, 1, i-1)] = substr($0, i+2)
+ next
+ }
+ header == 0 {
+ if ($0 ~ />+From /) sub(/>/, "")
+ var["Body"] = var["Body"] ? var["Body"] "\n" $0 : $0
+ }
+ END {
+ if (v) msg[v] = var["Body"]
+ if (arg0 == "graph") anc_graph()
+ else if (arg0 == "anc") query_anc(arg1, arg2)
+ else if (arg0 == "sup") query_sup(arg1, arg2)
+ else if (arg0 == "cli") query_cli(arg1, arg2)
+ else if (arg0 == "log") print msg[arg1]
+ else if (arg0 == "from") print from[arg1]
+ else if (arg0 == "date") print date[arg1]
+ else if (arg0 == "desc") printl(desc[arg1])
+ }
+ function anc_graph() {
+ print "digraph G {"
+ for (v in anc) print "\"" anc[v] "\" -> \"" v "\""
+ print "}"
+ }
+ function query_anc(v, n) { while (n-- > 0) v = anc[v]; print v }
+ function query_cli(v, n) {
+ while (n-- > 0 && v != "") {
+ num = split(v, av); v = ""
+ for (i = 1; i <= num; i++) {
+ printl(cli[av[i]])
+ v = v ? v " " cli[av[i]] : cli[av[i]]
+ }
+ }
+ }
+ function query_sup(v, n) {
+ while (n-- > 0 && v != "") {
+ num = split(v, av); v = ""
+ for (i = 1; i <= num; i++) {
+ printl(sup[av[i]])
+ v = v ? v " " sup[av[i]] : sup[av[i]]
+ }
+ }
+ }
+ function printl(l, i, n) {
+ n = split(l, al)
+ for (i = 1; i <= n; i++)
+ if (root[al[i]])
+ print al[i] " -> '"$PWD/"'" root[al[i]]
+ else
+ print al[i]
+ }'
+}
+
+initlib() {
+ rsync --list-only "$1/.dv." >/dev/null 2>&1 && return
+ mkdir -p /tmp/dv.$$
+ ln -s . /tmp/dv.$$/.dv.
+ >/tmp/dv.$$/.dvinfo.0
+ >/tmp/dv.$$/.dvinfo.1
+ chmod g+w /tmp/dv.$$/.dvinfo.[01]
+ rsync -a /tmp/dv.$$/ "$1"
+}
+
+isremote() { case $1 in (*:*) return;; esac; return 1; }
+
+help_all() {
+ printf "$dv_version\nUsage: dv command [options] [args]\n"
+ Opth=1; for c in $Cmdlist; do $c; done
+}
+
+newversion() {
+ [ "$OptV" ] && R1=$OptV && return
+ case $OptB in
+ ('') R1=$1 ;;
+ (*[-/]*) die 'illegal character [-/] in branch name' ;;
+ (*[0-9.]) die 'illegal end character [0-9.] in branch name' ;;
+ (*) case $1 in (*-${OptB}[1-9]*) R1=$1;; (*) R1=$1-${OptB}0;; esac ;;
+ esac
+ R2=${R1##*[!0-9]}
+ optb=$Optb
+ [ "$optb" ] && R3=.1 optb=${optb%b} || R3= R1=${R1%$R2}$(($R2 + 1))
+ while [ "${optb}" ]; do optb=${optb%b} R3=.0$R3; done
+ R1=$R1$R3
+ while grep -q "^Version: $item/$R1\$" /tmp/dv.$$/.dvinfo.0
+ do
+ R2=${R1##*[!0-9]}
+ [ "$R2" ] && R1=${R1%$R2}$(($R2 - 1)).1 || R1=${R1}1
+ done
+}
+
+patch() {
+ usage 'patch [-x pat] [-F File] [Dir|Version]' \
+ 'Print patch diff from ancestor' && return
+ diff "$@" | awk -v wdflag="-v${OptD:+3}${OptN:+1}${OptO:+2}" '
+ { key = $1; file = substr($0, length(key) + 2) }
+ key == "old" { old = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ key == "new" { new = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ {
+ of = old "/" file; gsub("'\''", "'\'\\\\\'\''", of)
+ nf = new "/" file; gsub("'\''", "'\'\\\\\'\''", nf)
+ system("diff -Naup '\''" of "'\'\ \''" nf "'\''; echo")
+ }'
+}
+
+save() {
+ usage 'save [-bln] [-A Author] [-m msg] [-x pat] [-B Branch|-V Version] [Dir]'\
+ 'Save dir into a new version' && return
+ [ "$2" ] && die 'too many arguments'
+ getpdir .dv "$1" || die "not in a clone: ${1:-$PWD}" && root=$R1
+ cd "$root"
+ getdv .dv && lib=$R1 && getdvinfo "$lib"
+ # Process supplier subdirs from the deepest up to "." (most client)
+ for d in $(find . -type f -name .dv | sort -r)
+ do
+ d=${d%/.dv}
+ cd "$root/$d"
+ OptF= save1 "$d" && nanc=$R1 && echo $nanc
+ [ "$d" = . ] && continue # not a supplier, done
+
+ # Replace supplier subdir by symlink in lib
+ getprd .dv && s=$R1/../.dv./$nanc
+ t=${d##*/}; t=.dv.$t
+ mv "$root/$d" "$root/${d%/*}/$t"
+ ln -s "$s" "$root/$d"
+ done
+ putdvinfo "$lib"
+ # Restore supplier subdirs and remove links to lib, deepest first
+ find . -type d -name ".dv.*" | sort -r | while read d
+ do
+ t=${d##*/.dv.}; t=${d%/*}/$t
+ rm -f "$t" && mv "$d" "$t"
+ done
+}
+
+save1() {
+ getdv .dv && lib=$R1 anc=$R2 && item=${anc%/*} old=${anc#*/}
+ [ "$(diffdir "$lib/$anc" .)" ] || { R1=$anc; return; }
+ newversion $old && new=$item/$R1
+ [ "$Optn" ] || dv_write "$new" "$anc" >.dv
+ # Compute list of direct suppliers, for caching in .dvinfo.0
+ lsup=$(find . -type l | while read -r l
+ do
+ case $l in (*/.dv.*) continue ;; esac
+ f=$(readlink "$l")
+ case $f in (*/.dv./*) printf "%s " ${f#*/.dv./} ;; esac
+ done)
+ [ ! "$Optn" ] && [ "$lsup" ] && dv_header . Sup "$lsup"
+ { cat .dv; echo; } >>/tmp/dv.$$/.dvinfo.0
+ [ -f ".dvignore" ] && xf=--exclude-from=.dvignore || xf=
+ rsync -aDS$Optn$Optv --link-dest=../$old $xf --exclude=".dv.*/" \
+ ./ "$lib/$new"
+ R1=$new
+ [ "$Optn" ] && return
+ isremote "$lib" && rsync -aDS --link-dest=../$old --exclude=".dv.*/" \
+ ./ "$cacheprefix/$lib/$new"
+ dv_header . Anc "$new"
+ dv_header . Lib "$lib"
+}
+
+supplier() {
+ usage 'supplier [Version]' 'print suppliers' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD) # create a temporary dvinfo file, query it
+ V=$R2; find . -name .dv |
+ while read dv
+ do
+ [ "$dv" = "./.dv" ] && continue
+ # get the supplier
+ getdv "$dv"; v=$R2; dv=${dv%/.dv}
+ # get the client
+ getpdir .dv "${dv%/*}"; getdv "$R1/.dv"
+ # declare the client as supplier
+ printf "From \nVersion: %s\nSup: %s\n" "$v" "$R2"
+ [ "$OptM" ] && echo "Root: ${dv#./}"; echo
+ done | info_query cli $V ${Opt_N:-9999} ;;
+ (*) info sup $1 ${Opt_N:-9999} ;;
+ esac | sort -u
+}
+
+usage() { [ "$Opth" ] && printf " %-34s %s\n" "$1" "$2"; }
+
+username() {
+ case $(uname -s) in
+ (Darwin) id -P | awk -v FS=: '{print $8}' ;;
+ (*) awk -v FS=[:,] '/^'$USER':/ {print $5; exit}' /etc/passwd ;;
+ esac
+}
+
+wdiff() {
+ usage 'wdiff [-DNO] [-x pat] [-F File] [Dir|Version]' \
+ 'Print word diffs from ancestor' && return
+ diff "$@" |
+ awk -v wdflag="-v${OptD:+3}${OptN:+1}${OptO:+2}" '
+ { key = $1; file = substr($0, length(key) + 2) }
+ key == "old" { old = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ key == "new" { new = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ {
+ of = old "/" file; gsub("'\''", "'\'\\\\\'\''", of)
+ nf = new "/" file; gsub("'\''", "'\'\\\\\'\''", nf)
+ system("wd " wdflag " '\''" of "'\'\ \''" nf "'\''; echo")
+ }' | less -FCimnqGrX -j5 -h0 +/'\[0'
+}
+
+cacheprefix=$HOME/.cache/dv
+Cmdlist='anc client clone desc diff info patch save supplier wdiff'
+while getopts :nvV opt # Parse global options
+do
+ case $opt in
+ (V) echo "$dv_version"; exit ;;
+ (*) help_all; exit ;;
+ esac
+done
+shift $((OPTIND - 1))
+[ $1 ] && C=$1 && shift 1 || { help_all; exit 1; }
+for c in $Cmdlist
+do
+ case $c in
+ ($C|_$C) cmd=$c; break;;
+ ($C*) [ $cmd ] && die ambiguous command $C || cmd=$c;;
+ esac
+done
+while getopts :0123456789A:bB:F:hl:m:MnNOsvV:x: opt # Parse command options
+do
+ case $opt in
+ ([0-9]) Opt_N=$Opt_N$opt;;
+ ([bhlnMNOsv]) eval Opt$opt=\${Opt$opt}$opt ;;
+ ([ABFmV]) eval Opt$opt=\$OPTARG ;;
+ (x) Optx="$Optx --exclude=$OPTARG" ;;
+ (*) Opth=1; $cmd; exit 1;;
+ esac
+done
+shift $(($OPTIND - 1))
+trap "rm -rf /tmp/dv.$$" EXIT
+[ "$cmd" ] || die "no command \"$C\"" && $cmd "$@"