#!/usr/bin/perl -w # # SubMaster Command Line Client - Proof of concept implementation # Copyright 2004 Clifford Wolf (GPL'ed) # use English; use strict; use POSIX qw(strftime); use File::Compare; use File::Copy; my $ondiskver = 3; my ($calldir, $basedir, $master); my $debug = (defined $ARGV[0] and $ARGV[0] eq "-d") ? 1 : 0; shift @ARGV if $debug; my $mode = $ARGV[0]; if ( defined $mode ) { shift @ARGV; } else { $mode = "help"; } sub dbg($) { print "D: $_[0]\n" if $debug; } sub run($) { dbg($_[0]); my $rc = system($_[0]); if ( $rc != 0 ) { print ">> ERROR Running '$_[0]' !!\n"; print ">> $!\n" if $rc == -1; exit 1; } } sub svn_get_head_rev($) { my $url = $_[0]; my $rev = `svn ls -v $url | tr -s ' ' | cut -f2 -d' ' | sort -n | tail -n 1`; chomp $rev; if ( $rev !~ /^[0-9]+$/ ) { print "Can't get current revision number for $url.\n"; exit 1; } return $rev; } if ($mode eq "add" or $mode eq "del" or $mode eq "diff" or $mode eq "st" or $mode eq "status" or $mode eq "revert" or $mode eq "resolved") { unshift(@ARGV, "$mode"); { exec "svn", @ARGV; }; print STDERR "couldn't exec svn: $!"; exit 127; } if ($mode ne "help" and `svn help > /dev/null; x=\$?; svm help > /dev/null; y=\$?; echo -n \$x\$y` ne "00") { print STDERR "Detected problems with calling 'svm' or 'svn'.\n"; print STDERR "Call 'sm help' for more information.\n"; exit 1; } if ($mode eq "create") { recreate_entry_point: ($master, $basedir) = @ARGV; my $localsvn = ""; if ($mode eq "create") { if ( -e "$basedir" ) { print "Can't create $basedir: file/directory exists already."; exit 1; } if ( -e "$basedir.sm" ) { print "Can't create $basedir.sm: file/directory exists already."; exit 1; } } my $last = svn_get_head_rev($master); run("mkdir -p $basedir.sm/SVN"); run("mkdir -p $basedir.sm/SM"); run("mkdir -p $basedir.sm/SENT"); run("mkdir -p $basedir.sm/WIP"); chomp($localsvn = `pwd`) if $basedir !~ /^\//; $localsvn .= "/$basedir.sm/SVN"; print "Creating local repository ...\n"; run("svnadmin create --fs-type fsfs $localsvn"); run("SVMREPOS='$localsvn' svm init MASTER $master"); print "Initial sync with master (may take some time) ...\n"; run("SVMREPOS='$localsvn' svm sync MASTER $last"); print "Creating local working copy ...\n"; run("svn copy -q -m 'Init /WORK' file://$localsvn/MASTER file://$localsvn/WORK"); run("svn co -q file://$localsvn/WORK $basedir"); print "Setting up submaster metadata ...\n"; my $lm_last = svn_get_head_rev("file://$localsvn/MASTER"); run("echo $ondiskver > $basedir.sm/SM/version.txt"); run("echo $master > $basedir.sm/SM/master.txt"); run("echo $lm_last > $basedir.sm/SM/sync.txt"); run("touch $basedir.sm/SM/queue.txt"); print "Type 'cd $basedir' now.\n" if $mode eq "recreate"; exit 0; } if ( $mode ne "help" ) { chomp($calldir = `pwd`); while (1) { chomp($basedir = `pwd`); last if -f "$basedir.sm/SM/sync.txt"; if ( $basedir eq "/" ) { print "Looks like this is not a SubMaster tree.\n"; exit 1; } chdir(".."); } chomp($master = `cat $basedir.sm/SM/master.txt`); my $thisondiskver = 1; chomp($thisondiskver = `cat $basedir.sm/SM/version.txt`) if -f "$basedir.sm/SM/version.txt"; # version 2 and 3 are compatible for 'recreate' if ( $mode eq "recreate" and $thisondiskver == 2 ) { $thisondiskver = 3; } if ( $thisondiskver ne $ondiskver ) { print "\nThis is not a version $ondiskver SubMaster tree!\n"; print "Check out the mailing lists for hints on\n"; print "converting older SubMaster trees...\n\n"; exit 1; } } if ($mode eq "recreate") { chdir("$basedir/.."); if ( open(MT, ") { if ( m, $basedir/, ) { print "\nLooks like there is still something ". "mounted in $basedir!\n"; print "--> aborting 'sm recreate'.\n\n"; exit 1; } } close MT; } print "Removing old data ...\n"; system "rm -rf $basedir $basedir.sm/SVN $basedir.sm/MASTER"; @ARGV = ($master, $basedir); goto recreate_entry_point; } sub refetch_master($) { my $current = $_[0]; my $old = `tr -d '\n' < $basedir.sm/SM/mrev.txt`; if ( $old ne $current ) { run("SVMREPOS='$basedir.sm/SVN' svm sync MASTER $current"); run("echo $current > $basedir.sm/SM/mrev.txt"); } } if ($mode eq "sync" or $mode eq "fsync") { my $oups=0; dbg("svn diff"); open(F,"svn diff|") || die $!; while () { print; $oups=1; } close F; if ( $oups ) { print "!!\n"; print "!! The working tree contains uncommited changes.\n"; print "!! Please commit or revert them first.\n"; print "!!\n"; exit 0; } print "Running 'svn up' (may take some time) ...\n"; run("svn up"); my $current = svn_get_head_rev($master); refetch_master($current); my $lm_current = svn_get_head_rev("file://$basedir.sm/SVN/MASTER"); if ($mode eq "sync") { my $lm_last = `tr -d '\n' < $basedir.sm/SM/sync.txt`; if ( $lm_last ne $lm_current ) { print "Merging changes $lm_last:$lm_current ($current) from local master ...\n"; run("svn merge -r $lm_last:$lm_current file://$basedir.sm/SVN/MASTER ."); print "Synced master rev $lm_last:$lm_current ($current) to local tree.\n\n"; print "Run 'svn commit -m \"SM Sync $lm_last:$lm_current ($current)\"' in $basedir now.\n\n"; run("echo $lm_current > $basedir.sm/SM/sync.txt"); } else { print "Already in sync with current master rev $lm_current ($current).\n"; } } else { if ( $#ARGV > -1 ) { foreach ( @ARGV ) { print "Syncing local with master: $_\n"; run("svn merge file://$basedir.sm/SVN/WORK/$_ file://$basedir.sm/SVN/MASTER/$_ $_"); } print "Part-Synced master rev $lm_current ($current) to local tree.\n\n"; print "Run 'svn commit -m \"SM Part-Sync $lm_current ($current)\"' in $basedir now.\n\n"; } else { print "Syncing local working copy with master ...\n"; run("svn merge file://$basedir.sm/SVN/WORK file://$basedir.sm/SVN/MASTER ."); print "Full-synced master rev $lm_current ($current) to local tree.\n\n"; print "Run 'svn commit -m \"SM Full-Sync $lm_current ($current)\"' in $basedir now.\n\n"; run("echo $lm_current > $basedir.sm/SM/sync.txt"); } } exit 0; } if ($mode eq "xdiff") { my $current = svn_get_head_rev($master); refetch_master($current); if ( $#ARGV > -1 ) { foreach ( @ARGV ) { run("svn diff file://$basedir.sm/SVN/MASTER/$_ file://$basedir.sm/SVN/WORK/$_"); } } else { run("svn diff file://$basedir.sm/SVN/MASTER file://$basedir.sm/SVN/WORK"); } exit 0; } if ($mode eq "commit" or $mode eq "instant") { chdir($calldir); run("svn commit ".join(" ",@ARGV)); @ARGV=(); chdir($basedir); chomp ($ARGV[0] = `svn log -qr HEAD file://$basedir.sm/SVN | cut -f1 -d' ' | tr -d 'r' | grep -v '^-----------'`); } if ( ($mode eq "patch" or $mode eq "instant" or ($#ARGV > 1 and $mode eq "wip" and $ARGV[0] eq "push")) and $#ARGV > -1 ) { my %queue; my $wipname = "XX"; if ($mode eq "wip") { $wipname = $ARGV[1]; shift @ARGV; shift @ARGV; if ( -f "$basedir.sm/WIP/$wipname.patch" ) { print "A WIP Patch with the name '$wipname' exists already.\n"; exit 1; } } if ($mode ne "instant") { open (F, "<$basedir.sm/SM/queue.txt") || die $!; while () { chomp; $queue{$_} = 1; } close F; } run("echo -n > __patch.tmp"); open(F, ">__message.tmp") || die $!; print F << "EOT"; --SMIGNORE-- --SMIGNORE-- Add a description for your patch below ... --SMIGNORE-- The following format is recommended for the description text: --SMIGNORE-- --SMIGNORE-- : --SMIGNORE-- Description of what Author1 did --SMIGNORE-- Description of what Author1 did --SMIGNORE-- : --SMIGNORE-- Description of what Author2 did --SMIGNORE-- ... --SMIGNORE-- EOT close F; foreach (@ARGV) { $_ = $1 if /^r([0-9]+)$/ and not -f $_; if ($mode ne "instant") { if ( not defined $queue{$_} and not -f $_ ) { print "$_ is neighter a file nor in the send queue!\n"; unlink "__patch.tmp"; unlink "__message.tmp"; exit 1; } delete $queue{$_}; } if ( -f $_ ) { run("sed -e '/^--- / Q;' < $_ >> __message.tmp"); } else { run("svn log -r $_ | egrep -v '^(----------|r[0-9]+ )' | tr -s '\n' >> __message.tmp"); } if ( -s "__patch.tmp" ) { rename("__patch.tmp", "__diff1.tmp"); if ( -f $_ ) { print "Dumping and merging $_ ...\n"; run("cat $_ > __diff2.tmp"); } else { print "Dumping and merging diff for revision $_ ...\n"; run("svn diff -r ".($_-1).":$_ file://$basedir.sm/SVN/WORK > __diff2.tmp"); } dbg("combinediff -q __diff1.tmp __diff2.tmp > __patch.tmp"); if ( system("combinediff -q __diff1.tmp __diff2.tmp > __patch.tmp") != 0 ) { print "Can't merge diff for revision $_!\n"; print "See __*.tmp to debug the problem.\n"; exit 1; } unlink "__diff1.tmp"; unlink "__diff2.tmp"; } else { if ( -f $_ ) { print "Dumping $_ ...\n"; run("cat $_ > __patch.tmp"); } else { print "Dumping diff for revision $_ ...\n"; run("svn diff -r ".($_-1).":$_ file://$basedir.sm/SVN/WORK > __patch.tmp"); } } } run("echo '' >> __message.tmp"); if ($mode ne "instant") { run("\${EDITOR:-vi} __message.tmp"); } my $pf = strftime "%y%m%d_%H%M%S_$$.patch", localtime; $pf = "WIP/$wipname.patch" if $mode eq "wip"; $_ = $basedir; s/.*\///; print "Writing patch to $_.sm/$pf ...\n"; run("filterdiff __patch.tmp | grep -hv ^--SMIGNORE-- __message.tmp - > $basedir.sm/$pf"); unlink "__message.tmp"; unlink "__patch.tmp"; if ($mode ne "instant") { open (F, ">$basedir.sm/SM/queue.txt") || die $!; foreach (sort {$a <=> $b} keys %queue) { print F $_,"\n"; } close F; } if ($mode eq "wip") { @ARGV = ( "$basedir.sm/$pf" ); $mode = "wip_rapply"; } else { exit 0; } } if ($mode eq "wip" and $#ARGV == 1 and $ARGV[0] eq "ci") { system("mv '$ARGV[1].patch' '$basedir.sm/WIP/'"); exit 0; } if ($mode eq "wip" and $#ARGV == 1 and $ARGV[0] eq "co") { system("mv '$basedir.sm/WIP/$ARGV[1].patch' ."); exit 0; } if (($mode eq "patch" or $mode eq "wip") and $#ARGV == -1) { my $x = $mode eq "wip" ? "WIP/" : ""; while (<$basedir.sm/$x*.patch>) { open(F, $_) || die $!; s/.*\///; s/\.patch$//; print "Patch ", $_, ":\n"; while () { last if /\S/; } print; while () { last unless /\S/; print; } print "-----------------------------------------------\n"; close F; } exit 0; } if ($mode eq "apply" or $mode eq "wip_rapply" or ($mode eq "wip" and $#ARGV == 1 and $ARGV[0] eq "pull")) { my $rarg = $mode eq "wip_rapply" ? "-R" : ""; my ($p, $last_patchfile); my $flist = ""; shift @ARGV if $mode eq "wip"; foreach $p (@ARGV) { $p = "$basedir.sm/WIP/$p.patch" if $mode eq "wip"; $last_patchfile = $p; print "\nTest-applying $p ...\n"; run("patch $rarg -Efp0 --dry-run < $p"); print "\nApplying $p ...\n"; run("patch $rarg -Efp0 < $p"); print "\nRunning svn add and svn del commands ...\n"; open(F, "lsdiff -Es $p|") || die "$!"; while () { die "Unexpected lsdiff output" unless /^([-+!])\s+(\S+)/; my ($op, $fn) = ($1, $2); $op =~ y/+-/-+/ if $mode eq "wip_rapply"; sub svn_add_dir($); sub svn_add_dir($) { my $d = $_[0]; $d =~ s,/[^/]+$,,; return if -d "$d/.svn"; svn_add_dir($d); run("svn add -N $d"); $flist .= " $d"; } svn_add_dir("$fn") if $op eq "+"; run("svn add $fn") if $op eq "+"; run("svn del $fn") if $op eq "-"; $flist .= " $fn"; } close F; } run("svn commit -m 'Pushed $ARGV[0] to WIP archive' $flist") if $mode eq "wip_rapply"; if ( $mode eq "wip" ) { open(I, "<$last_patchfile") || die $!; open(O, ">__sm_wip_commit_msg.txt") || die $!; while () { last if /\S/; } print O; while () { print O; last unless /\S/; } close I; close O; run("svn commit -F __sm_wip_commit_msg.txt $flist"); unlink("__sm_wip_commit_msg.txt"); unlink("$last_patchfile"); @ARGV=(); chdir($basedir); $mode = "queue"; chomp ($ARGV[0] = `svn log -qr HEAD file://$basedir.sm/SVN | cut -f1 -d' ' | tr -d 'r' | grep -v '^-----------'`); } else { exit 0; } } if ($mode eq "queue" or $mode eq "commit") { my $verbose = ($#ARGV >= 0 and $ARGV[0] eq "-v") ? "-v" : ""; shift @ARGV if $verbose eq "-v"; if ( $#ARGV >= 0 ) { my %queue; open (F, "<$basedir.sm/SM/queue.txt") || die $!; while () { s/[^0-9]//g; $queue{$_} = 1; } close F; foreach (@ARGV) { s/[^0-9]//g; $queue{$_} = 1; } open (F, ">$basedir.sm/SM/queue.txt") || die $!; foreach (sort {$a <=> $b} keys %queue) { print F $_,"\n"; } close F; } else { open (F, "<$basedir.sm/SM/queue.txt") || die $!; while () { chomp; run("svn log $verbose -r $_ file://$basedir.sm/SVN |tail -n +2|tr -s '\\n'"); } close F; } exit 0; } if ($mode eq "admin" or $mode eq "send" or $mode eq "get") { my ($url, $user, $pass); if ( -f "$basedir.sm/SM/server.txt" ) { open(F, "$basedir.sm/SM/server.txt") || die $!; chomp($url=); chomp($user=); chomp($pass=); $url =~ s/ +$//; $user =~ s/ +$//; $pass =~ s/ +$//; close F; } else { print << "EOT"; >> >> $basedir.sm/SM/server.txt not found! >> >> Create/edit that file: >> 1. line: SubMaster URL (just the directory, without "/smadm.cgi?...") >> 2. line: username >> 3. line: password >> EOT exit 1; } if ($mode eq "admin" ) { system("\${BROWSER:-w3m -o confirm_qq=0 -cookie} '$url/smadm.cgi?u=$user&p=$pass'"); } if ($mode eq "send") { print "Server URL: $url\n"; if ( $#ARGV == -1 ) { while (<$basedir.sm/*.patch>) { s/.*\///; s/\.patch$//; push @ARGV, $_; } } foreach (@ARGV) { if ( ! -f "$basedir.sm/$_.patch" ) { print "Patch not found: $_\n"; next; } print "\nUploading patch $_ ...\n"; open(F, "curl -k -F u=$user -F p=$pass -F a=new -F q=1 ". "-F f=\@$basedir.sm/$_.patch $url/smadm.cgi |"); my ($patchid, $line); while ($line = ) { $patchid=$1 if $line =~ /^Patch (\S+) added./; print $line; } close F; if ( defined $patchid ) { rename "$basedir.sm/$_.patch", "$basedir.sm/SENT/$patchid.patch"; } else { print "Error while uploading patch: $_\n"; } } } if ( $mode eq "get") { foreach (@ARGV) { if (!/^([0-9]{4})[\/_]?([0-9]{2})[\/_]?([0-9]+)$/) { print "ID in wrong format: $_\n---\n"; next; } my ($a, $b) = ("$1/$2/$3.patch", "$1$2$3.patch"); print "Getting $b...\n$url/data/$a\n"; run("curl -k -s -o $b $url/data/$a"); print "---\n"; } } exit 0; } print << 'EOT'; SubMaster Command Line Client - Proof of concept implementation Copyright 2004, 2005 Clifford Wolf (GPL'ed) sm create creates a local copy of the given parent svn. The must not exist already and will be created by this command. sm sync run this command in your working copy directory to sync with the latest changes in the parent subversion. make sure that you don't have any un-commited changes when running this command. just run "svn commit" (not "sm commit") after resolving all conflicts (if there are any). sm fsync [ dir .. ] use this command with care! it makes a full sync of your working copy with the master tree. that means that all local changes will be overwritten! think twice before doing that. If a directory is specified (names relative to the base directory), only the given directories will be synced. sm recreate this is like fsync, but also drops the local subversion database and recreates it. this will remove your local history - so use with care! sm xdiff [ dir .. ] creates a diff between your working directory and the master. If a directory is specified (names relative to the base directory), only the given directories will be diffed. The diff could be used by make a manual fsync, e.g. sm commit run an "svn commit" and add this revision to the send queue. always use that command instead of a simple "svn commit". sm queue ... add the given revision numbers to the send queue manually. this is e.g. usefull if you took back an already sent patch or commited changes directly using "svn commit". usually this is done automatically by "sm commit". sm queue [-v] this views the current send queue. usualy you want to display that list before running "sm patch". sm patch .. remove the given revisions from the send queue and create a patch file. instead of revision numbers it's also possible to specify patch files (or mix both). This is very usefull if you want to "extend" a patch you created earlier. sm instant like "sm commit", but directly creates the patch without touching the queue. sm patch list the patch files waiting to be sent to the submaster server. sm send [ ..] send patches to the submaster server (needs curl). If the patchfile is ommited, all pending patches are send to the server. sm get .. downloads the specified patch from the submaster server and saves the patch file in the base directory of the working copy. This is useful if you want to modify a patch somebody else has already commited to the submaster system. sm wip list all patches from the WIP (work-in-progres) archive. This is a place where you can store non-finished stuff until you get time to continue working at them. sm wip ci sm wip co check-in (move from working directory to WIP archive) or check-out (move from WIP archive to working directory) a patch. sm wip push name .. like "sm patch", but stores the patch in the WIP archive and reverts it in the working copy. sm wip pull name applies a patch from WIP archive in the working directory and removes it from WIP archive. sm apply [..] applies the specified patch file to your working tree. this also runs all required 'svn add' and 'svn del' commands. sm admin opens the submaster server admin interface (needs w3m). sm { add | del | diff | st | status | revert | resolved } pass as-is to svn program This tool needs working 'svn' and a working 'svm' commands. The former is part of the Subversion package, the latter is part of the SVN::Mirror perl module. EOT exit 0 if $mode eq "help"; exit 1;