#!/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 "$@"