#!/bin/bash # # svnbranch.sh - helper script for managing subversion branching # Copyright (C) 2007 Clifford Wolf # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or set -e export LC_ALL=C callsvn_cachedir="" svncmd="svn" verbose=0 cacherequested=0 rotategraph=0 while true; do if [ "$1" = "-x" ]; then set -x shift; continue fi if [ "$1" = "-v" ]; then verbose=1 shift; continue fi if [ "$1" = "-r" ]; then rotategraph=1 shift; continue fi if [ "$1" = "-cache" ]; then cacherequested=1 svncmd="svncache" shift; continue fi break done callsvn() { if [ $verbose = 1 ]; then echo "> $*" >&2 fi if [ -n "$callsvn_cachedir" ]; then local cacheid="`echo "$*" | cksum | tr -d ' '`" if [ ! -f "$callsvn_cachedir/$cacheid" ]; then $svncmd "$@" > "$callsvn_cachedir/$cacheid" fi cat "$callsvn_cachedir/$cacheid" else $svncmd "$@" fi } nl=" " url_to_parent_ret="" url_to_parent_url="" url_to_parent_root="" url_to_parent_info_args="" url_to_parent() { local info=`callsvn info $url_to_parent_info_args "$1"` local url=`echo "$info" | grep '^URL: ' | sed 's,.*: ,,'` local rev=`echo "$info" | grep '^Last Changed Rev: ' | sed 's,.*: ,,'` local root=`echo "$info" | grep '^Repository Root: ' | sed 's,.*: ,,'` local path="${url#$root}" if [ -z "$url" -o -z "$rev" -o -z "$root" -o "$path" = "$url" ]; then echo "SVNBRANCH FATAL ERROR IN: url_to_parent '$1'" >&2 exit 1 fi url_to_parent_url="$url" url_to_parent_root="$root" url_to_parent_ret="$path@$rev" } if [ "$1" = "branch" -a -n "$2" ] && [ $cacherequested = 0 ]; then url_to_parent . callsvn propset svnbranch:parent "$url_to_parent_ret" . callsvn -q propdel svnbranch:tag . callsvn copy . "$2" # FIXME: Is it possible to use 'svn revert' only on the # svnbranch:parent and svnbranch:tag properties? callsvn revert . exit 0 fi if [ "$1" = "tag" -a -n "$2" ] && [ $cacherequested = 0 ]; then url_to_parent . callsvn propset svnbranch:parent "$url_to_parent_ret" . callsvn propset svnbranch:tag "1" . callsvn copy . "$2" # FIXME: Is it possible to use 'svn revert' only on the # svnbranch:parent and svnbranch:tag properties? callsvn revert . exit 0 fi if [ "$1" = "init" -a -n "$2" ] && [ $cacherequested = 0 ]; then url_to_parent "$2" callsvn propset svnbranch:parent "$url_to_parent_ret" . exit 0 fi if [ "$1" = "sync" ] && [ $cacherequested = 0 ]; then if [ -n "`callsvn propget svnbranch:tag .`" ]; then echo "Not syncing a tag: $PWD" >&2 exit 1 fi old_parent=`callsvn propget svnbranch:parent .` if [ -z "$old_parent" ]; then echo "Not a branch of anything: $PWD" >&2 exit 1 fi if [ -z "$2" ]; then url_to_parent "." url_to_parent_info_args="-r HEAD" url_to_parent "$url_to_parent_root$old_parent" else url_to_parent "$2" fi callsvn merge "$url_to_parent_root$old_parent" "$url_to_parent_root$url_to_parent_ret" . # FIXME: This is for resolving the svnbranch:parent merge conflict. # To bad that it seams to be impossible to revert the property only. $svncmd revert . callsvn propset svnbranch:parent "$url_to_parent_ret" . exit 0 fi if [ "$1" = "diff" ]; then url_to_parent "." parent=`callsvn propget svnbranch:parent .` if [ -z "$parent" ]; then echo "Not a branch of anything: $PWD" >&2 exit 1 fi if [ $# = 1 ]; then callsvn diff "$url_to_parent_root$parent" "$url_to_parent_url" else prev=${parent##*@}; parent=${parent%@*} if [ -n "$prev" ]; then prev="@$prev"; fi shift; for file; do callsvn diff "$url_to_parent_root$parent/$file$prev" "$url_to_parent_url/$file" done fi exit 0 fi if [ "$1" = "merge" -a -n "$2" ] && [ $cacherequested = 0 ]; then if [ -n "`callsvn propget svnbranch:tag .`" ]; then echo "Not merging to a tag: $PWD" exit 1 fi url_to_parent "$2" parent=`callsvn propget svnbranch:parent "$2"` callsvn merge "$url_to_parent_root$parent" "$2" . # FIXME: This is for resolving the svnbranch:parent merge conflict. # To bad that it seams to be impossible to revert the property only. $svncmd revert . exit 0 fi list="" generate_list() { local url="$1" filter="$2" if [ "${url#$filter}" != "$url" ]; then local parent=`callsvn propget svnbranch:index $url` if [ -n "parent" ]; then if [ -n "$list" ]; then list="$list$nl" fi list="$list$url" fi fi for url in `callsvn propget svnbranch:index "$1" | grep . | perl -le ' my $filter = '"'$filter'"'; sub svnasterisk($$) { my ($a,$b) = @_; my $ret=""; return "" if index($a, $filter) and index($filter, $a); open(P,". '"'$callsvn_cachedir/api.sh'"'; callsvn ls '\''$_[0]'\''|"); while (

) { next unless s/\/\n$//; $ret.="$a$_$b\n"; } close P; return $ret; } while (<>) { $_='\'"$1/"\''.$_; 1 while s!^(.*?/|)\*(.*)\n?!svnasterisk($1,$2)!me; foreach (split /\n/) { print unless index($_, $filter) and index($filter, $_);; } }'` do generate_list "$url" "$filter" done } show_list() { callsvn_cachedir="`mktemp -d`" { declare -f callsvn; declare -p verbose svncmd callsvn_cachedir; } > "$callsvn_cachedir/api.sh" if [ -z "$*" ]; then url_to_parent "." generate_list "$url_to_parent_root" "$url_to_parent_url" else for url in "$@"; do if [ "$url" = "/" ]; then url_to_parent "." url="$url_to_parent_root" fi url_to_parent "$url" generate_list "$url_to_parent_root" "$url_to_parent_url" done fi { while read url; do parent=`callsvn propget svnbranch:parent "$url"` url_to_parent_info_args="" url_to_parent "$url" if [ -n "$parent" ]; then if [ -n "`callsvn propget svnbranch:tag "$url"`" ]; then echo "TAG $parent $url_to_parent_ret" else echo "BRANCH $parent $url_to_parent_ret" fi url_to_parent_info_args="-r HEAD" url_to_parent "$url" echo "HEAD $url_to_parent_ret" url_to_parent "$url_to_parent_root$parent" echo "HEAD $url_to_parent_ret" fi done < <( echo "$list" | grep .; ) } | sort -u rm -rf "$callsvn_cachedir" callsvn_cachedir="" } if [ "$1" = "list" ]; then shift show_list "$@" exit 0 fi if [ "$1" = "graph" -o "$1" = "show" ]; then outdir=""; mode="$1"; shift if [ "$mode" = "show" ]; then outdir="`mktemp -d`/" fi echo "Generating ${outdir}svnbranch.dot." if [ "$*" = "-" ]; then cat else show_list "$@" fi | perl -e ' my @history; my @branches; my %head_nodes; my %nodes; my %tags; my $nc=0; while (<>) { if (/^TAG (\S+) (\S+)/) { $tags{$2} = 1; } if (/^(BRANCH|TAG) (\S+) (\S+)/) { $nodes{$2} = $nc++; $nodes{$3} = $nc++; push @branches, [ $2, $3]; } if (/^HEAD (\S+)/) { $head_nodes{$1} = 1; $nodes{$1} = $nc++; } } print "digraph G {\n"; foreach $n (keys %nodes) { if ($n =~ /(.*)\@(.*)/) { my ($base, $rev) = ($1, $2); my $history_prev_node; my $history_prev_rev = -1; foreach (keys %nodes) { next unless /(.*)\@(.*)/; if ($1 eq $base && $2 < $rev && $2 > $history_prev_rev) { $history_prev_node = $_; $history_prev_rev = $2; } } if (defined $history_prev_node) { push @history, [ $history_prev_node, $n ]; } } print "node${nodes{$n}} "; if (defined $head_nodes{$n}) { my $x = $n; $x =~ s/\@.*//; print "[label=\"$x\", shape=box"; print ", style=filled" if defined $tags{$n}; } else { print "[label=\"$n\", shape=octagon"; print ", style=filled, fillcolor=red" if defined $tags{$n}; } print "];\n"; } foreach (@history) { my ($prev, $next) = @$_; print "node${nodes{$prev}} -> node${nodes{$next}} [color=green, style=bold];\n"; } foreach (@branches) { my ($parent, $branch) = @$_; if (defined $head_nodes{$parent} and not defined $tags{$branch} != '$rotategraph') { print "node${nodes{$parent}} -> node${nodes{$branch}} [color=blue, style=bold];\n"; } else { print "node${nodes{$parent}} -> node${nodes{$branch}} [color=blue, style=bold, minlen=0];\n"; } } print "}\n"; ' > ${outdir}svnbranch.dot echo "Generating ${outdir}svnbranch.ps." dot -Tps -o ${outdir}svnbranch.ps ${outdir}svnbranch.dot if [ "$mode" = "show" ]; then ( kghostview ${outdir}svnbranch.ps; rm -rf "$outdir"; ) & fi exit 0 fi if [ "$1" = "propagate" ]; then svncmd="svn" url_to_parent "." start_url="${url_to_parent_url#$url_to_parent_root}" cat > propagate.sh << EOT #!/bin/bash # # Auto-generated by svnbranch - do not change! url_root="$url_to_parent_root" last_ok=1000 cmd="x" EOT cat >> propagate.sh << 'EOT' cleancheck() { local ret=0 while read line; do ret=1 echo "$line" done < <( svn st --no-ignore | grep -v '^\(\? propagate\.sh\| [CM] \.\)$'; ) return $ret } V() { echo "> $*" "$@" } X() { if [ $1 -gt $last_ok ]; then echo echo "***" echo "*** $2" echo "***" if ! cleancheck; then echo "Cleancheck failed!" exit 1 fi V svn switch "${url_root}$2" || exit 1 V svn up || exit 1 V svnbranch sync || exit 1 if cleancheck; then V svn revert . || exit 1 else while [ "$cmd" != "y" -a "$cmd" != "a" ]; do echo "*** Changes synced to: $2 ***" read -p "Commit this to the Repository? ([Y]es, [A]ll, [Q]uit) " cmd [ "$cmd" == "Y" ] && cmd="y" [ "$cmd" == "A" ] && cmd="a" [ "$cmd" == "Q" ] && exit 1 [ "$cmd" == "q" ] && exit 1 done V svn commit -m 'svnbranch sync (via svnbranch propagate)' || exit 1 [ "$cmd" == "y" ] && cmd="x" fi sed -i "s,^last_ok=.*,last_ok=$1," $0 || exit 1 fi } svn up || exit 1 if ! cleancheck; then echo "Cleancheck failed!" exit 1 fi EOT if [ -n "$2" ]; then cat "$2" else show_list "$url_to_parent_root" fi | perl -e ' my %path; my $nr = 1000; while (<>) { if (/^BRANCH (\S+) (\S+)/) { my ($parent, $child) = ($1, $2); $parent =~ s/@.*//; $child =~ s/@.*//; push @{$path{$parent}}, $child; } } sub rec($) { my $parent = $_[0]; foreach my $child (@{$path{$parent}}) { printf "X %d \"%s\"\n", ++$nr, $child; rec($child); } } rec("'"$start_url"'"); print "\necho\n"; print "echo \"***\"\n"; print "echo \"*** FINISH\"\n"; print "echo \"***\"\n"; print "V svn switch \"\${url_root}'"$start_url"'\"\n\n"; ' >> propagate.sh chmod +x propagate.sh exit 0 fi cat << 'EOT' SVNBRANCH HELP ============== Svnbranch is a small shellscript for managing complex subversion branch and merge szenarios. It is using two directory properties to hold its metadata: svnbranch:parent The absolute (i.e. relative to the repository root) path to the directory from which this directory has been branched off. This path always contains a peg revision ("@RevNum" suffix). svnbranch:tag Set to '1' on tags (and does not exist on branches). A tag is technically the same as a branch but does not follow its parent (i.e. 'sync' is an invalid operation on a tag). svnbranch:index A list of relative pathnames to subdirectories which do have svnbranch:index or svnbranch:parent properties. They must be set explicitely by the administrator and are used for gathering a list of all branches and parents in the 'list', 'show' and 'graph' modes. Svnbranch evaluates only '*' wildcards which match an entire directory in svnbranch:index. The svnbranch call semantic is pretty simmilar to the one of 'svn', the first argument beeing a mode of operation followed by arguments: svnbranch branch URL Creates a branch of the current working directory with the specified URL in the repository. This basically is an 'svn cp . URL' with the svnbranch:parent property set to the working copy URL and revision. svnbranch tag URL Creates a tag of the current working directory with the specified URL in the repository. It does the same thing as 'svnbranch branch' but also sets the svnbranch:tag property. svnbranch init URL Create a svnbranch:parent property in the current working copy and use the specified URL as parent. svnbranch sync [URL] Only valid in a working copy of a branch. Syncs changes made in the parent into the branch and updates the svnbranch:parent property. When a URL is given the parent is changed to the specified parent. This is basically an 'svn merge OLD-PARENT NEW-PARENT' combined with an updated of the svnbranch:parent property. svnbranch [-cache] diff [FILE..] Only valid in a working copy of a branch. Shows the difference between the branch and its parent. svnbranch merge URL Merge the changes from the branches parent to the branch into the current working directory. svnbranch [-cache] list [URL..] Use the svnbranch:index properties to create a list of all branches below the current working directory (or the given URL) and show their relations. This is issueing a lot of calls to 'svn'. So do not wonder when this commands (and its variations below) take a while to complete. Use '/' as URL to create the list for the entire repository. svnbranch [-cache] [-r] graph [URL..] Like 'svnbranch list' but using graphviz to graph the data. With '-' as argument this reads a prior generated 'svnbranch list' output as input and graph it. svnbranch [-cache] [-r] show [URL..] Like 'svnbranch graph' but also open the graph in kghostview. svnbranch [-cache] propagate [filename] Generates a propagate.sh script for propagating all changes comitted to the local directory to non-tag branches and stop at the first conflict. The conflicts can be resolved and the respective changes commited manually. After that propagate.sh can be restarted. It will simply continue where it has stopped. The output of an 'svnbranch list' with all relevant information can be passed as command line argument. If it is missing, 'svnbranch propagate' will do a repository-wide 'svnbranch list' (which may take a while). Calling svnbranch with the option -v (before the mode) makes svnbranch write out a message for every 'svn' command it invokes. Calling svnbranch with the option -cache (for the modes which support it) makes svnbranch use 'svncache' instead of 'svn'. Calling svnbranch with the option -r (for the graph modes) rotates the graph so that tags are plotted vertically and branches are plotted vertically. EOT exit 1