#!/bin/sh # - Usage: dv command [options] [args] # - dv is a dumb version management tool. # - Options: # - -a num set ancestor level (default 1) # - -n dry-run mode, no changes are made # - -v verbose mode # - Commands: unset CDPATH export LC_ALL=C IFS=' ' version=dv-0.2 dvlib=${DVLIB:-$HOME/dvlib} script="$(cd "$(dirname "$0")" && pwd)/${0##*/}" aopt=1 case $(uname -o) in (Android|Linux) diffopt='--color=always' lsopt='-v --color' ;; (Darwin) diffopt='--color=always' lsopt='-G' ;; (*BSD) ;; esac # anc [-a num] [dir] - prints the current ancestor of dir or current. anc() { set -- "$(dvdir "$1")" for _ in $(seq "$aopt"); do set -- "$1/.dv/anc" done [ -L "$1" ] && readlink -f "$1" } # clone [-nv] v1 [dir] - clones an iterm version into dir (default: item). clone() { case $1 in ('') die 'missing argument' ;; (*[/.-]*) set -- "$(path "$1")" "$2" ;; (*) set -- "$(head "$1")" "$2" ;; esac [ "$2" ] || { set -- "$1" "${1##*/}"; set -- "$1" "${2%%-*}"; } [ -d "$1" ] || die "not found: $1" rsync ${vopt:+"$vopt"} ${nopt:+"$nopt"} -a "$1/" "$2/" rm -f "$2/.dv/anc" && ln -s "$1" "$2/.dv/anc" } # desc - prints the descendants of a version desc() { set -- "$(path "$1")" set -- "$1" "${1%/*}" set -- "$1" "${2##*/}" for a in "$dvlib/$2"/*/.dv/anc*; do [ "$a" -ef "$1" ] && echo "${a%/*/*}" done } die() { echo "$@" >&2 exit 1 } # diff [-a num] [[v1] v2] - prints differences (default: current and ancestor). diff() { if [ "$2" ]; then set -- "$(path "$2")" "$(path "$1")" elif [ "$1" ]; then set -- "$(path "$1")" "$(anc "$(path "$1")")" else set -- "$(dvdir)" "$(anc)" fi [ "$2" ] && command diff $diffopt -U 3 -x .dv "$2" "$1" | less -F } # dvdir prints the dir containing .dv. dvdir() { set -- "$(realpath "${1:-.}")" while [ "$1" ]; do [ -d "$1/.dv" ] && echo "$1" && return set -- "${1%/*}" done } # head - prints the branch head versions. head() { for v in "$dvlib/${1:-$(name)}/"*; do [ "$(desc "$v")" ] || echo "$v" done } # help - prints this help text. help() { while read -r line; do case $line in (\#*\ -\ *) ;; (*) continue ;; esac set -- "${line% - *}" "${line#* - }" [ "$1" = '#' ] || printf " %-26s" "${1#\# }" echo "$2" done < "$script" } # incv increments version. incv() { set -- "$1" "${1##*[-.a-z_A-Z]}" set -- "${1%"$2"}" "$2" echo "$1$(($2 + 1))" } # init - creates an initial clone in the current directory. init() { [ "$(dvdir)" ] && die "already initialized: $(dvdir)" mkdir .dv } # ls [item] - lists dvlib. ls() ( cd "$dvlib" && command ls $lsopt "$@" ) # name prints the item name. name() { set -- "$(dvdir)" [ "$1" ] && echo "${1##*/}" || die "not a dv item, no .dv found" } # path prints the version path, if exist. path() { case $1 in (*/*|.) dvdir "$1"; return ;; ([0-9]*) set -- "$(name)-$1" ;; esac set -- "${1%%-*}" "${1#*-}" [ -d "$dvlib/$1/$1-$2" ] && echo "$dvlib/$1/$1-$2" } # save [-nv] [v1] - creates a new version from clone. save() { [ "$1" ] && case $1 in (*[0-9]) ;; (*) die "invalid version: $1" ;; esac set -- "$(dvdir)" "$(aopt=1 anc)" "$(name)" "${1#[a-zA-Z_]*-}" set -- "$1" "$2" "$3" "${4:-$(incv "${2##*/[a-zA-Z_]*-}")}" echo "$dvlib/$3/$3-$4" command diff -q -x .dv "$2" "$1" >/dev/null 2>&1 && die 'no changes, abort' [ "$nopt" ] && return # Update ancestor in clone then copy in dvlib. rm -f "$1/.dv/anc" && [ "$2" ] && ln -s "../../${2##*/}" "$1/.dv/anc" rsync ${vopt:+"$vopt"} -a ${2:+--link-dest="$2"} --mkpath "$1/" "$dvlib/$3/$3-$4/" # After save, clone ancestor points to new saved version. rm -f "$1/.dv/anc" && ln -s "$dvlib/$3/$3-$4" "$1/.dv/anc" } # status - prints dv informations. status() { echo "lib: $dvlib" set -- "$(dvdir)" "$(anc)" [ "$1" ] || die "not a dv item, no .dv found" echo "root: $1" [ "$2" ] || die "no ancestor found, please save it first" echo "anc: $2" command diff -q -x .dv "$2" "$1" } # sync [url] - synchronize a remote dvlib. sync() { set -- "${1:-$(cat "$dvlib/.url" 2>/dev/null)}" [ "$1" ] || die "missing url" [ "$(cat "$dvlib/.url" 2>/dev/null)" = "$1" ] || echo "$1" > "$dvlib/.url" rsync ${vopt:+"$vopt"} ${nopt:+"$nopt"} -aH --mkpath "$1/" "$dvlib/" rsync ${vopt:+"$vopt"} ${nopt:+"$nopt"} -aH --mkpath "$dvlib/" "$1/" } # version - prints the current version of dv. version() { echo "$version" } # Main program starts here. [ "$DVDEBUG" ] && set -x || case $1 in (anc|clone|desc|diff|head|help|init|ls|path|save|status|sync|version) ;; (*) help; exit 1;; esac cmd="$1" shift while getopts :a:nv opt; do case $opt in (a) aopt="$OPTARG" ;; (n) nopt=-n ;; (v) vopt=-v ;; (*) help; exit ;; esac done shift $((OPTIND - 1)) $cmd "$@" # Todo: # * a function to squash a branch in the trunk # * merge another ancestor (use anc0, anc1, ...) # * vendor management # * show graphs (dot) # * sync: dvlib sync / mirror: resolve conflict by branching # * rename version. # * .dvignore # * make dvlib versions immutable.