/* Copyright (c) 2007, Yauheni Akhotnikau THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import tango.core.Exception; import tango.core.Thread; import tango.io.Console; import tango.io.Conduit; import tango.io.FilePath; import tango.io.FileSystem; import tango.io.Stdout; import tango.sys.Process; import tango.util.time.Timer; import tango.text.convert.Sprint; import tango.text.stream.LineIterator; alias Sprint!(char) Formatter; Formatter makeFormatter() { return new Formatter; } Timer * startTimer() { auto t = new Timer; t.start; return t; } // Exception for a case when process finishes with non-zero status. // Exception stores content of STDERR of terminated process. class NonZeroProcessStatusException : TracedException { this( char[] msg, char[][] stderr ) { super( msg ); stderr_ = stderr.dup; } char[][] stderr() { return stderr_; } private : char[][] stderr_; } // Launches specified process and returns content of its STDOUT. // Throws NonZeroProcessStatusException if process finishes with // non-zero status. char[][] launchProcess( char[][] commandLine, char[] workingPath = null ) { char[][] readProcessStream( Conduit conduit ) { char[][] r; foreach( line; new LineIterator!(char)( conduit ) ) r ~= line; return r; } auto p = new Process( commandLine ); if( workingPath ) p.workDir = workingPath; p.execute(); auto output = readProcessStream( p.stdout ); auto stderr = readProcessStream( p.stderr ); auto launchResult = p.wait(); if( 0 != launchResult.status ) throw new NonZeroProcessStatusException( "Process '" ~ p.toUtf8 ~ "' non-zero status: '" ~ launchResult.toUtf8 ~ "'", stderr ); return output; } // Starts 'svn' command with specified list of additional arguments. // If workingPath isn't null then svn started in that path. // // Returns contents of svn STDOUT. char[][] svn( char[][] additionalArgs, char[] workingPath = null ) { return launchProcess( [ "svn" ] ~ additionalArgs ~ [ "--non-interactive" ], workingPath ); } // Same as svn() but adds line 'Time: XXXs' to svn STDOUT. char[][] timedSvn( char[][] additionalArgs, char[] workingPath = null ) { auto t = startTimer; auto r = svn( additionalArgs, workingPath ); r ~= makeFormatter.format( "Time: {0}s", t.stop ).dup; return r; } // Helper function to copy the specified lines to Stdout. // Returns Stdout. typeof(Stdout) linesToStdout( char[][] lines ) { foreach( l; lines ) Stdout( l ).newline; return Stdout; } // Searches the specified key in 'svn info' output. char[] keyValue( char[][] svnInfo, char[] key ) { char[] prefix = key ~ ": "; foreach( infoLine; svnInfo ) if( infoLine.length > prefix.length && prefix == infoLine[ 0..prefix.length ] ) return infoLine[ prefix.length..$ ].dup; throw new NoSuchElementException( "Key '" ~ key ~ "' not found" ); } // Gets result of 'svn ls' and return list with names of paths. char[][] selectOnlyPaths( char[][] entries ) { char[][] result; foreach( entry; entries ) if( '/' == entry[ $-1 ] ) result ~= entry.dup; return result; } // Base class for threads which perform some actions with Subversion. // Each thread must known the repository URL. // Each thread produces some output. class SvnActionRunnerThread : Thread { this( char[] repositoryUrl ) { super( &run ); repositoryUrl_ = repositoryUrl.dup; } char[][] output() { return output_; } protected : char[] repositoryUrl_; char[][] output_; abstract void run(); } // Thread for updating some subpath in working copy. class SubpathUpdateThread : SvnActionRunnerThread { this( char[] repositoryUrl, char[] path ) { super( repositoryUrl ); path_ = path.dup; } protected : char[] path_; override void run() { output_ ~= makeFormatter.format( "Updating existed path: {0}", path_ ).dup; output_ ~= timedSvn( [ "up" ], path_ ); } } // Thread for checking out missing subpaths. class SubpathCheckOutThread : SvnActionRunnerThread { this( char[] repositoryUrl, char[][] paths ) { super( repositoryUrl ); paths_ = paths.dup; } protected : char[][] paths_; override void run() { foreach( path; paths_ ) { output_ ~= makeFormatter.format( "Checking out missing path: {0}", path ).dup; output_ ~= timedSvn( [ "co", repositoryUrl_ ~ "/" ~ path ] ); } } } // Helper for catching exception from joined thread. // Returns 0 if no exception, and 1 if NonZeroProcessStatusException // exception have been caught. int joinThreadGuard( void delegate() joinBody ) { try { joinBody(); } catch( NonZeroProcessStatusException x ) { Stdout( "Exception: " ~ x.toUtf8 ~ "\nStderr:\n" ); foreach( line; x.stderr ) Stdout( "\t" ~ line ~ "\n" ); Stdout.flush; return 1; } return 0; } // Helper template for creating new thread of type THREAD_TYPE, // starting it and adding reference to it in the specified storage. template ThreadStarter( alias THREAD_TYPE ) { void startNewAndAppendTo(SECOND_ARG, STORAGE_TYPE)( char[] repositoryUrl, SECOND_ARG arg, inout STORAGE_TYPE storage ) { auto t = new THREAD_TYPE( repositoryUrl, arg ); t.start; storage ~= t; } } // Returns count of detected errors. int tryUpdateSubpaths( char[] repositoryUrl, char[][] subpaths ) { SvnActionRunnerThread[] threads; char[][] missingSubpaths; // First step: run SubpathUpdateThread for each existing path and // collect names of missing paths. foreach( path; subpaths ) { if( ( new FilePath( path, true ) ).exists ) ThreadStarter!(SubpathUpdateThread).startNewAndAppendTo( repositoryUrl, path, threads ); else missingSubpaths ~= path; } // Second step: run SubpathCheckOutThread for all missing paths. if( 0 != missingSubpaths.length ) ThreadStarter!(SubpathCheckOutThread).startNewAndAppendTo( repositoryUrl, missingSubpaths, threads ); // The last step: collect results form all threads. int errorsCount = 0; foreach( t; threads ) { errorsCount += joinThreadGuard( { t.join( true ); linesToStdout( t.output ).flush; } ); } return errorsCount; } // Returns count of detected errors. int doParallelUpdate() { auto totalTime = startTimer; // Checking workspace revision. auto workcopyInfo = svn( [ "info" ] ); auto repositoryUrl = keyValue( workcopyInfo, "URL" ); // Selecting names of subdirectories to be updated. auto subpaths = selectOnlyPaths( svn( [ "ls", repositoryUrl ] ) ); // And updating them. auto errorsCount = tryUpdateSubpaths( repositoryUrl, subpaths ); // Now updating root path if no errors detected. if( !errorsCount ) { Stdout( "Updating root" ).newline; linesToStdout( timedSvn( [ "up", "-N" ] ) ).flush; } Stdout.format( "Total time: {0}\n", totalTime.stop ).flush; return errorsCount; } int run( char[][] args ) { if( 2 == args.length ) { // It is necessary to change current path. auto previousPath = FileSystem.getDirectory(); FileSystem.setDirectory( args[ 1 ] ); scope(exit) FileSystem.setDirectory( previousPath ); return doParallelUpdate(); } else // All work must be done in the current directory. return doParallelUpdate(); } int main( char[][] args ) { try { if( 2 < args.length ) { Stdout( "SVN Parallel Update. Yauheni Akhotnikau 2007\n\n" "Usage:\n" "\tsvnparup [working-copy-path]\n\n" "Examples:\n" "for updating current directory:\n" "\tsvnparup\n" "for updating working copy in ~/sandboxes/cool_prj:\n" "\tsvnparup ~/sandboxes/cool_prj\n" ). flush; return 1; } else return run( args ); } catch( Exception x ) { Cerr( "*** Exception caught:\n\t" )( x ).newline.flush; return 2; } return 0; } // vim:ts=2:sts=2:sw=2:expandtab:fenc=utf-8: