/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "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 * COPYRIGHT OWNER OR CONTRIBUTORS 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. */ //some functions taken/adapted from http://wiki.svnkit.com/Managing_A_Working_Copy /* * ==================================================================== * Copyright (c) 2004-2008 TMate Software Ltd. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://svnkit.com/license.html * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * ==================================================================== */ package net.sourceforge.kolmafia.svn; import java.io.BufferedReader; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.TreeMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JOptionPane; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.KoLmafiaCLI; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.StaticEntity; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.utilities.FileUtilities; import net.sourceforge.kolmafia.utilities.PauseObject; import net.sourceforge.kolmafia.utilities.StringUtilities; import org.tmatesoft.svn.core.ISVNLogEntryHandler; import org.tmatesoft.svn.core.SVNDepth; import org.tmatesoft.svn.core.SVNDirEntry; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.SVNNodeKind; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory; import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.wc.ISVNOptions; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNEventAction; import org.tmatesoft.svn.core.wc.SVNInfo; import org.tmatesoft.svn.core.wc.SVNRevision; import org.tmatesoft.svn.core.wc.SVNStatusType; import org.tmatesoft.svn.core.wc.SVNUpdateClient; import org.tmatesoft.svn.core.wc.SVNWCUtil; import org.tmatesoft.svn.core.wc.ISVNEventHandler; public class SVNManager { static final Lock SVN_LOCK = new ReentrantLock(); private static final int RETRY_LIMIT = 3; private static final int DEPENDENCY_RECURSION_LIMIT = 5; private static Stack<SVNFileEvent> eventStack = new Stack<SVNFileEvent>(); private static TreeMap<File, Long[]> updateMessages = new TreeMap<File, Long[]>(); private static SVNClientManager ourClientManager; //private static ISVNEventHandler myCommitEventHandler; private static ISVNEventHandler myUpdateEventHandler; private static ISVNEventHandler myWCEventHandler; private static Pattern SOURCEFORGE_PATTERN = Pattern.compile( "/p/(.*?)/(?:code|svn)(.*)", Pattern.DOTALL ); private static Pattern GOOGLECODE_HOST_PATTERN = Pattern.compile( "([^\\.]+)\\.googlecode\\.com", Pattern.DOTALL ); private static List<String> permissibles = Arrays.asList( "scripts", "data", "images", "relay", "ccs", "planting" ); /** * Initializes the library to work with a repository via different protocols. */ public synchronized static void setupLibrary() { if ( ourClientManager != null ) return; /* * For using over http:// and https:// */ DAVRepositoryFactory.setup(); /* * For using over svn:// and svn+xxx:// */ SVNRepositoryFactoryImpl.setup(); ISVNOptions options = SVNWCUtil.createDefaultOptions( true ); /* * Creates an instance of SVNClientManager providing authentication information (name, password) and an options * driver */ ourClientManager = SVNClientManager.newInstance( options ); myUpdateEventHandler = new UpdateEventHandler(); myWCEventHandler = new WCEventHandler(); /* * Sets a custom event handler for operations of an SVNUpdateClient instance */ ourClientManager.getUpdateClient().setEventHandler( myUpdateEventHandler ); /* * Sets a custom event handler for operations of an SVNWCClient instance */ ourClientManager.getWCClient().setEventHandler( myWCEventHandler ); } /** * Meant to be called before any operation that interacts with a remote repository. Prepares client manager and * cleans up static variables in case they were not cleaned up in the previous operation. */ private static void initialize() { if ( ourClientManager == null ) { setupLibrary(); } eventStack.clear(); updateMessages.clear(); } /* * Creates a new version controlled directory (doesn't create any intermediate directories) right in a repository. * Like 'svn mkdir URL -m "some comment"' command. It's done by invoking SVNCommitClient.doMkDir(SVNURL[] urls, * String commitMessage) which takes the following parameters: urls - an array of URLs that are to be created; * commitMessage - a commit log message since a URL-based directory creation is immediately committed to a * repository. */ /* private static SVNCommitInfo makeDirectory( SVNURL url, String commitMessage ) throws SVNException { * Returns SVNCommitInfo containing information on the new revision committed (revision number, etc.) return ourClientManager.getCommitClient().doMkDir( new SVNURL[] { url }, commitMessage ); }*/ /* * Imports an unversioned directory into a repository location denoted by a destination URL (all necessary parent * non-existent paths will be created automatically). This operation commits the repository to a new revision. Like * 'svn import PATH URL (-N) -m "some comment"' command. It's done by invoking SVNCommitClient.doImport(File path, * SVNURL dstURL, String commitMessage, boolean recursive) which takes the following parameters: path - a local * unversioned directory or singal file that will be imported into a repository; dstURL - a repository location * where the local unversioned directory/file will be imported into; this URL path may contain non-existent parent * paths that will be created by the repository server; commitMessage - a commit log message since the new * directory/file are immediately created in the repository; recursive - if true and path parameter corresponds to a * directory then the directory will be added with all its child subdirictories, otherwise the operation will cover * only the directory itself (only those files which are located in the directory). */ /* private static SVNCommitInfo importDirectory( File localPath, SVNURL dstURL, String commitMessage, boolean isRecursive ) throws SVNException { * Returns SVNCommitInfo containing information on the new revision committed (revision number, etc.) return ourClientManager.getCommitClient().doImport( localPath, dstURL, commitMessage, isRecursive ); }*/ /* * Committs changes in a working copy to a repository. Like 'svn commit PATH -m "some comment"' command. It's done * by invoking SVNCommitClient.doCommit(File[] paths, boolean keepLocks, String commitMessage, boolean force, * boolean recursive) which takes the following parameters: paths - working copy paths which changes are to be * committed; keepLocks - if true then doCommit(..) won't unlock locked paths; otherwise they will be unlocked after * a successful commit; commitMessage - a commit log message; force - if true then a non-recursive commit will be * forced anyway; recursive - if true and a path corresponds to a directory then doCommit(..) recursively commits * changes for the entire directory, otherwise - only for child entries of the directory; */ /* private static SVNCommitInfo commit( File wcPath, boolean keepLocks, String commitMessage ) throws SVNException { * Returns SVNCommitInfo containing information on the new revision committed (revision number, etc.) return ourClientManager.getCommitClient().doCommit( new File[] { wcPath }, keepLocks, commitMessage, false, true ); }*/ /* * Checks out a working copy from a repository. Like 'svn checkout URL[@REV] PATH (-r..)' command; It's done by * invoking SVNUpdateClient.doCheckout(SVNURL url, File dstPath, SVNRevision pegRevision, SVNRevision revision, * boolean recursive) which takes the following parameters: url - a repository location from where a working copy is * to be checked out; dstPath - a local path where the working copy will be fetched into; pegRevision - an * SVNRevision representing a revision to concretize url (what exactly URL a user means and is sure of being the URL * he needs); in other words that is the revision in which the URL is first looked up; revision - a revision at * which a working copy being checked out is to be; recursive - if true and url corresponds to a directory then * doCheckout(..) recursively fetches out the entire directory, otherwise - only child entries of the directory; */ public static long checkout( SVNURL url, SVNRevision revision, File destPath, boolean isRecursive ) throws SVNException { int wcFormat = Preferences.getInteger("svnFormat"); if ( wcFormat == 0 ) { wcFormat = -1; } SVNUpdateClient updateClient = getClientManager().getUpdateClient(); /* * sets externals not to be ignored during the checkout */ updateClient.setIgnoreExternals( false ); /* * returns the number of the revision at which the working copy is */ return updateClient.doCheckout( url, destPath, revision, revision, SVNDepth.fromRecurse( isRecursive ), false, wcFormat ); } /* * Updates a working copy (brings changes from the repository into the working copy). Like 'svn update PATH' * command; It's done by invoking SVNUpdateClient.doUpdate(File file, SVNRevision revision, boolean recursive) which * takes the following parameters: file - a working copy entry that is to be updated; revision - a revision to which * a working copy is to be updated; recursive - if true and an entry is a directory then doUpdate(..) recursively * updates the entire directory, otherwise - only child entries of the directory; */ public static long update( File wcPath, SVNRevision updateToRevision, boolean isRecursive ) throws SVNException { return update( wcPath, updateToRevision, isRecursive, 0 ); } private static long update( File wcPath, SVNRevision updateToRevision, boolean isRecursive, int retryCount ) throws SVNException { if ( ourClientManager == null ) { setupLibrary(); } SVNUpdateClient updateClient = ourClientManager.getUpdateClient(); /* * sets externals not to be ignored during the update */ updateClient.setIgnoreExternals( false ); /* * returns the number of the revision wcPath was updated to */ long rev = -1; try { rev = updateClient.doUpdate( wcPath, updateToRevision, SVNDepth.fromRecurse( isRecursive ), false, false ); } catch ( SVNException e ) { if ( e.getErrorMessage().getErrorCode() == SVNErrorCode.ATOMIC_INIT_FAILURE && retryCount <= RETRY_LIMIT ) { retryCount++ ; // workaround for stupid sourceforge Apache bug RequestLogger.printLine( "Server-side error during svn update, retrying " + retryCount + " of " + RETRY_LIMIT ); return update( wcPath, updateToRevision, isRecursive, retryCount ); } else throw e; } return rev; } /* * Updates a working copy to a different URL. Like 'svn switch URL' command. It's done by invoking * SVNUpdateClient.doSwitch(File file, SVNURL url, SVNRevision revision, boolean recursive) which takes the * following parameters: file - a working copy entry that is to be switched to a new url; url - a target URL a * working copy is to be updated against; revision - a revision to which a working copy is to be updated; recursive * - if true and an entry (file) is a directory then doSwitch(..) recursively switches the entire directory, * otherwise - only child entries of the directory; */ /* private static long switchToURL( File wcPath, SVNURL url, SVNRevision updateToRevision, boolean isRecursive ) throws SVNException { SVNUpdateClient updateClient = ourClientManager.getUpdateClient(); * sets externals not to be ignored during the switch updateClient.setIgnoreExternals( false ); * returns the number of the revision wcPath was updated to return updateClient.doSwitch( wcPath, url, updateToRevision, isRecursive ); }*/ /* * Collects status information on local path(s). Like 'svn status (-u) (-N)' command. It's done by invoking * SVNStatusClient.doStatus(File path, boolean recursive, boolean remote, boolean reportAll, boolean includeIgnored, * boolean collectParentExternals, ISVNStatusHandler handler) which takes the following parameters: path - an entry * which status info to be gathered; recursive - if true and an entry is a directory then doStatus(..) collects * status info not only for that directory but for each item inside stepping down recursively; remote - if true then * doStatus(..) will cover the repository (not only the working copy) as well to find out what entries are out of * date; reportAll - if true then doStatus(..) will also include unmodified entries; includeIgnored - if true then * doStatus(..) will also include entries being ignored; collectParentExternals - if true then externals definitions * won't be ignored; handler - an implementation of ISVNStatusHandler to process status info per each entry * doStatus(..) traverses; such info is collected in an SVNStatus object and is passed to a handler's * handleStatus(SVNStatus status) method where an implementor decides what to do with it. */ /* private static void showStatus( File wcPath, boolean isRecursive, boolean isRemote, boolean isReportAll, boolean isIncludeIgnored, boolean isCollectParentExternals ) throws SVNException { * StatusHandler displays status information for each entry in the console (in the manner of the native * Subversion command line client) ourClientManager.getStatusClient().doStatus( wcPath, isRecursive, isRemote, isReportAll, isIncludeIgnored, isCollectParentExternals, new StatusHandler( isRemote ) ); }*/ /* * Collects information on local path(s). Like 'svn info (-R)' command. It's done by invoking * SVNWCClient.doInfo(File path, SVNRevision revision, boolean recursive, ISVNInfoHandler handler) which takes the * following parameters: path - a local entry for which info will be collected; revision - a revision of an entry * which info is interested in; if it's not WORKING then info is got from a repository; recursive - if true and an * entry is a directory then doInfo(..) collects info not only for that directory but for each item inside stepping * down recursively; handler - an implementation of ISVNInfoHandler to process info per each entry doInfo(..) * traverses; such info is collected in an SVNInfo object and is passed to a handler's handleInfo(SVNInfo info) * method where an implementor decides what to do with it. */ public static void showInfo( File wcPath, SVNRevision revision ) throws SVNException { /* * InfoHandler displays information for each entry in the console (in the manner of the native Subversion * command line client) */ getClientManager().getWCClient().doInfo( wcPath, SVNRevision.UNDEFINED, revision, SVNDepth.INFINITY, null, new InfoHandler() ); } /** * A wrapper for doInfo so that callers do not have to handle the client manager (or interface with the SVN_LOCK). */ public static SVNInfo doInfo( File projectFile ) throws SVNException { try { SVN_LOCK.lock(); return getClientManager().getWCClient().doInfo( projectFile, SVNRevision.WORKING ); } finally { SVN_LOCK.unlock(); } } public static void showCommitMessage( File wcPath, long from, long to ) throws SVNException { if ( !KoLmafia.permitsContinue() ) return; // we don't want to show the commit info from the revision that we're on if we're moving to another revision. // example: going from r13 -> r15 : we don't want to show r13's info. if ( from < to ) ++from; // alternately, if we're decreasing in revision, we don't want to show the revision that we came from. // example: going from r15 -> r14 : we don't want to show r15's info. if ( from > to ) --from; getClientManager().getLogClient().doLog( new File[]{wcPath}, SVNRevision.create( from ), SVNRevision.create( to ), true, false, 10, new ISVNLogEntryHandler() { public void handleLogEntry( SVNLogEntry logEntry ) throws SVNException { RequestLogger.printLine( "Commit <b>r" + logEntry.getRevision() + "<b>:" ); RequestLogger.printLine( "Author: " + logEntry.getAuthor() ); RequestLogger.printLine(); RequestLogger.printLine( logEntry.getMessage() ); RequestLogger.printLine( "------" ); } } ); } /* * Puts directories and files under version control scheduling them for addition to a repository. They will be added * in a next commit. Like 'svn add PATH' command. It's done by invoking SVNWCClient.doAdd(File path, boolean force, * boolean mkdir, boolean climbUnversionedParents, boolean recursive) which takes the following parameters: path - * an entry to be scheduled for addition; force - set to true to force an addition of an entry anyway; mkdir - if * true doAdd(..) creates an empty directory at path and schedules it for addition, like 'svn mkdir PATH' command; * climbUnversionedParents - if true and the parent of the entry to be scheduled for addition is not under version * control, then doAdd(..) automatically schedules the parent for addition, too; recursive - if true and an entry is * a directory then doAdd(..) recursively schedules all its inner dir entries for addition as well. */ /* private static void addEntry( File wcPath ) throws SVNException { ourClientManager.getWCClient().doAdd( wcPath, false, false, false, true ); }*/ /* * Locks working copy paths, so that no other user can commit changes to them. Like 'svn lock PATH' command. It's * done by invoking SVNWCClient.doLock(File[] paths, boolean stealLock, String lockMessage) which takes the * following parameters: paths - an array of local entries to be locked; stealLock - set to true to steal the lock * from another user or working copy; lockMessage - an optional lock comment string. */ /* private static void lock( File wcPath, boolean isStealLock, String lockComment ) throws SVNException { ourClientManager.getWCClient().doLock( new File[] { wcPath }, isStealLock, lockComment ); }*/ /* * Schedules directories and files for deletion from version control upon the next commit (locally). Like 'svn * delete PATH' command. It's done by invoking SVNWCClient.doDelete(File path, boolean force, boolean dryRun) which * takes the following parameters: path - an entry to be scheduled for deletion; force - a boolean flag which is set * to true to force a deletion even if an entry has local modifications; dryRun - set to true not to delete an entry * but to check if it can be deleted; if false - then it's a deletion itself. */ /* private static void delete( File wcPath, boolean force ) throws SVNException { if ( ourClientManager == null ) { setupLibrary(); } ourClientManager.getWCClient().doDelete( wcPath, force, false ); }*/ public static void doCleanup() { initialize(); RequestThread.postRequest( new CleanupRunnable() ); } public static void doCheckout( SVNURL repo ) { initialize(); RequestThread.postRequest( new CheckoutRunnable( repo ) ); pushUpdates( true ); if ( Preferences.getBoolean( "svnInstallDependencies" ) ) checkDependencies(); } private static void showCommitMessages() { for ( File f : updateMessages.keySet() ) { if ( updateMessages.get( f ) == null || updateMessages.get( f )[0] <= 0 ) { continue; } RequestLogger.printLine( "Update log for <b>" + f.getName() + "</b>:" ); RequestLogger.printLine( "------" ); try { SVN_LOCK.lock(); showCommitMessage( f, updateMessages.get( f )[ 0 ], updateMessages.get( f )[ 1 ] ); } catch ( SVNException e ) { error( e ); } finally { SVN_LOCK.unlock(); } } updateMessages.clear(); } /** * There are only 5 permissible files in the top level of the repo, all of them directories: scripts/, data/, ccs/, * images/, and relay/. Any other files (directories or otherwise) in the top-level fails validation. * TODO: check * file extensions, reject naughty ones */ static boolean validateRepo( SVNURL repo ) { return validateRepo( repo, false ); } private static boolean validateRepo( SVNURL repo, boolean quiet ) { SVNRepository repository; Collection< ? > entries; boolean failed = false; try { SVN_LOCK.lock(); repository = getClientManager().createRepository( repo, true ); } catch ( SVNException e ) { if ( !quiet ) error( e, "Unable to connect with repository at " + repo.getPath() ); return true; } finally { SVN_LOCK.unlock(); } if ( !quiet ) RequestLogger.printLine( "Validating repo..." ); try { SVN_LOCK.lock(); entries = repository.getDir( "", -1, null, (Collection< ? >) null ); // this syntax is stupid, by the way } catch ( SVNException e ) { if ( !quiet ) error( e, "Something went wrong while fetching svn directory info" ); return true; } finally { SVN_LOCK.unlock(); } Iterator< ? > iterator = entries.iterator(); while ( iterator.hasNext() ) { SVNDirEntry entry = (SVNDirEntry) iterator.next(); if ( entry.getKind().equals( SVNNodeKind.DIR ) ) { failed |= !permissibles.contains( entry.getName() ); } else // something other than a directory // we allow a single top-level file to declare dependencies, nothing else failed = !entry.getName().equals( "dependencies.txt" ); } if ( failed && !quiet ) { KoLmafia.updateDisplay( MafiaState.ERROR, "The requested repo failed validation. Complain to the script's author." ); } else { if ( !quiet ) RequestLogger.printLine( "Repo validated." ); } return failed; } private static void pushUpdates() { pushUpdates( false ); } /** * Method to copy all queued updates from the individual Working Copy folders to the mafia subdirectories. * <p> * If a checkout was performed, we automatically push ALL files to subdirectories. If an update was performed, we * push new files (SVNEventAction.UPDATE_ADD) always, and updates (SVNEventAction.UPDATE_UPDATE) iff the file still * exists in the destination subdirectory. If the user deleted it, we don't want to re-add it. * <p> * For updates (SVNEventAction.UPDATE_UPDATE), also handle all three possible change results (SVNStatusType.CHANGED, * SVNStatusType.CONFLICTED, and SVNStatusType.MERGED). * * @param wasCheckout is <b>true</b> if a checkout was performed first. */ private static void pushUpdates( boolean wasCheckout ) { if ( eventStack.isEmpty() ) { RequestLogger.printLine( "Done." ); return; } // make a copy of the event stack - we'll need this later for the optional info step @SuppressWarnings( "unchecked" ) Stack<SVNFileEvent> eventStackCopy = (Stack<SVNFileEvent>) SVNManager.eventStack.clone(); List<String> pathsToSkip = doFinalChecks( wasCheckout ); if ( pathsToSkip.size() > 0 ) { RequestLogger.printLine( "NOTE: Skipping some updates due to user request." ); } RequestLogger.printLine( "Pushing local updates..." ); while ( !eventStack.isEmpty() ) { SVNFileEvent event = eventStack.pop(); if ( event.getFile().isDirectory() ) { continue; // directories will be generated by mkdirs(), no need to explicitly create them } if ( event.getFile().getParentFile().getParentFile().equals( KoLConstants.SVN_LOCATION ) ) { continue; // no top-level files get pushed - including dependencies.txt } // we now need to obtain the file's path relative to one of the four "permissible" folders // iterate up f until we find the depth we want to be at File fDepth = findDepth( event.getFile().getParentFile() ); if ( fDepth == null ) { eventStack.clear(); return; } // get the path of the file relative to the project folder. // example: C:/mafia/svn/<projectname>/scripts/dir1/dir2/scriptfile.ash becomes scripts/dir1/dir2/scriptfile.ash String relpath = FileUtilities.getRelativePath( fDepth.getParentFile(), event.getFile() ); if ( pathsToSkip.contains( relpath ) ) { // user wants to skip it RequestLogger.printLine( "Skipping " + relpath ); continue; } if ( shouldPush( event, wasCheckout, relpath ) ) { doPush( event.getFile(), relpath ); } else if ( shouldDelete( event ) ) { doDelete( event.getFile(), relpath ); } } RequestLogger.printLine( "Done." ); eventStack.clear(); // use the copy of the event stack that we made earlier to show commit info // no need to try to show commit messages for checkouts if ( !wasCheckout ) queueCommitMessages( eventStackCopy ); } private static void queueCommitMessages( Stack<SVNFileEvent> eventStackCopy ) { if ( eventStackCopy.isEmpty() ) return; if ( !Preferences.getBoolean( "svnShowCommitMessages" ) ) { SVNManager.updateMessages.clear(); return; } /* * We need to turn this stack of files and events into {workingCopy, revision[]} pairs, where workingCopy = a * File that represents the root of the working copy that was updated, and revision[] = two Long that represent * the revision that we started at and the revision that we're going to. In other words, we need to collapse all * of the individual file events into one object per working copy. */ TreeMap<File, Long[]> feMap = new TreeMap<File, Long[]>(); while ( !eventStackCopy.isEmpty() ) { SVNFileEvent fe = eventStackCopy.pop(); File f = fe.getFile(); while ( !f.getParentFile().equals( KoLConstants.SVN_LOCATION ) ) { f = f.getParentFile(); if ( f == null ) // shouldn't happen, punt return; } // some file events can be "add" events where from is -1 // "delete" events will also have to == -1 long from = fe.getEvent().getPreviousRevision(); long to = fe.getEvent().getRevision(); if ( fe.getEvent().getAction().equals( SVNEventAction.UPDATE_ADD ) || fe.getEvent().getAction().equals( SVNEventAction.UPDATE_DELETE ) ) { // just get the notes from the revision instead of everything between -1 and the revision if ( from == -1 && to != -1) from = to; else if ( to == -1 && from != -1 ) to = from; else continue; } // assume that getPreviousRevision is the same for every file feMap.put( f, new Long[] { from, to } ); } SVNManager.updateMessages.putAll( feMap ); } private static File findDepth( File f ) { return findDepth( f, false ); } private static File findDepth( File f, boolean force ) { String original = f.getAbsolutePath(); foundDepth : { while ( f != null ) { // look two directories up. If it is the svn folder, we're at the level we want to be. if ( f.getParentFile() != null && f.getParentFile().getParentFile() != null && f.getParentFile().getParentFile().equals( KoLConstants.SVN_LOCATION ) ) break foundDepth; f = f.getParentFile(); } RequestLogger.printLine( "Internal error: could not find relative path for " + original + ". Aborting." ); return null; } if ( !force && !permissibles.contains( f.getName() ) ) { //shouldn't happen. Validation should have failed. RequestLogger.printLine( "Non-permissible folder in SVN root: " + f.getName() + " Stopping local updates." ); return null; } return f; } /** * If an update (not a checkout) results in svn adding a new file from the repo, make sure the user is okay with it. * <p> * This is to hopefully safeguard against malicious script injection, but the user has to catch it. * * @return a <b>List</b> of files to skip. */ private static List<String> doFinalChecks( boolean wasCheckout ) { if ( wasCheckout ) { return checkExisting(); } List<String> skipFiles = new ArrayList<String>(); List<SVNURL> skipURLs = new ArrayList<SVNURL>(); @SuppressWarnings( "unchecked" ) // no type-safe way to do this in Java 5 (6 has Deque) Stack<SVNFileEvent> eventStackCopy = (Stack<SVNFileEvent>) SVNManager.eventStack.clone(); while ( !eventStackCopy.isEmpty() ) { SVNFileEvent event = eventStackCopy.pop(); if ( event.getFile().isDirectory() ) { continue; // directories are harmless } if ( event.getFile().getParentFile().getParentFile().equals( KoLConstants.SVN_LOCATION ) ) { continue; // no top-level files get pushed - including dependencies.txt } File fDepth = findDepth( event.getFile().getParentFile() ); if ( fDepth == null ) { //shouldn't happen, punt return skipFiles; } // We only want to prompt if the file is new to the working copy (SVNEventAction.UPDATE_ADD) // as this most likely means that it was recently added to the repo // SVNEventAction.ADD is UPDATE_ADD for binary files if ( event.getEvent().getAction() == SVNEventAction.UPDATE_ADD || event.getEvent().getAction() == SVNEventAction.ADD ) { skipFiles.add( FileUtilities.getRelativePath( fDepth.getParentFile(), event.getFile() ) ); skipURLs.add( event.getEvent().getURL() ); } } if ( skipFiles.size() > 0 ) { SVNRepository repo = null; try { SVN_LOCK.lock(); repo = getClientManager().createRepository( skipURLs.get( 0 ), true ); } catch ( SVNException e ) { error( e, "Something went wrong fetching SVN info" ); //punt, NPE ensues if we continue with this method without initializing repo return skipFiles; } finally { SVN_LOCK.unlock(); } StringBuilder message = new StringBuilder( "<html>New file(s) requesting confirmation:<p>" ); int extra = 0; for ( int i = 0; i < skipFiles.size(); ++i ) { if ( i > 9 ) { extra += 1; continue; } message.append( "<b>file</b>: " + skipFiles.get( i ) + "<p>" ); try { SVN_LOCK.lock(); repo.setLocation( skipURLs.get( i ), false ); SVNDirEntry props = repo.info( "", -1 ); if ( props == null || props.getAuthor() == null ) message.append( "<b>author</b>: unknown<p>" ); else message.append( "<b>author</b>: " + props.getAuthor() + "<p>" ); } catch ( SVNException e ) { error( e, "Something went wrong fetching SVN info" ); } finally { SVN_LOCK.unlock(); } } if ( extra > 0 ) { message.append( "<b>and " + extra + " more...</b>" ); } //message.append( "<br>SVN info:<p>" ); try { SVN_LOCK.lock(); SVNURL root = repo.getRepositoryRoot( false ); message.append( "<br><b>repository url</b>:" + root.getPath() + "<p>" ); } catch ( SVNException e ) { error( e, "Something went wrong fetching SVN info" ); } finally { SVN_LOCK.unlock(); } message.append( "<br><b>Only click yes if you trust the author.</b>" + "<p>Clicking no will stop the files from being added locally. (until you checkout the project again)" ); if ( JOptionPane.showConfirmDialog( null, message, "SVN wants to add new files", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE ) == JOptionPane.YES_OPTION ) { skipFiles.clear(); } } return skipFiles; } /** * When a user does svn checkout, he/she may not want project files to overwrite existing local files. Therefore, * warn if local files exist. * * @return a <b>List</b> of relpaths to ignore */ private static List<String> checkExisting() { List<String> skipFiles = new ArrayList<String>(); List<SVNURL> skipURLs = new ArrayList<SVNURL>(); @SuppressWarnings( "unchecked" ) // no type-safe way to do this in Java 5 (6 has Deque) Stack<SVNFileEvent> eventStackCopy = (Stack<SVNFileEvent>) SVNManager.eventStack.clone(); while ( !eventStackCopy.isEmpty() ) { SVNFileEvent event = eventStackCopy.pop(); if ( event.getFile().isDirectory() ) { continue; // directories are harmless } if ( event.getEvent().getAction() != SVNEventAction.UPDATE_ADD ) { continue; // we only care about ADD events } if ( event.getFile().getParentFile().getParentFile().equals( KoLConstants.SVN_LOCATION ) ) { continue; // no top-level files get pushed - including dependencies.txt } File fDepth = findDepth( event.getFile().getParentFile() ); if ( fDepth == null ) { //shouldn't happen, punt return skipFiles; } String relpath = FileUtilities.getRelativePath( fDepth.getParentFile(), event.getFile() ); File rebase = getRebase( relpath ); if ( rebase == null ) continue; // We only want to prompt if the file already exists locally if ( rebase.exists() ) { skipFiles.add( relpath ); skipURLs.add( event.getEvent().getURL() ); } } if ( skipFiles.size() > 0 ) { SVNRepository repo = null; try { SVN_LOCK.lock(); repo = getClientManager().createRepository( skipURLs.get( 0 ), true ); } catch ( SVNException e ) { error( e, "Something went wrong fetching SVN info" ); //punt, NPE ensues if we continue with this method without initializing repo return skipFiles; } finally { SVN_LOCK.unlock(); } StringBuilder message = new StringBuilder( "<html>New file(s) will overwrite local files:<p>" ); for ( int i = 0; i < skipFiles.size(); ++i ) { File rebase = SVNManager.getRebase( skipFiles.get( i ) ); String rerebase = FileUtilities.getRelativePath( KoLConstants.ROOT_LOCATION , rebase ); message.append( "<b>file</b>: " + rerebase + "<p>" ); try { SVN_LOCK.lock(); repo.setLocation( skipURLs.get( i ), false ); SVNDirEntry props = repo.info( "", -1 ); message.append( "<b>author</b>: " + props.getAuthor() + "<p>" ); } catch ( SVNException e ) { error( e, "Something went wrong fetching SVN info" ); } finally { SVN_LOCK.unlock(); } } try { SVN_LOCK.lock(); SVNURL root = repo.getRepositoryRoot( false ); message.append( "<br><b>repository url</b>:" + root.getPath() + "<p>" ); } catch ( SVNException e ) { error( e, "Something went wrong fetching SVN info" ); } finally { SVN_LOCK.unlock(); } message.append( "<br>Checking out this project will result in some local files (described above) being overwritten." + "<p>Click yes to overwrite them, no to skip installing them." ); if ( JOptionPane.showConfirmDialog( null, message, "SVN checkout wants to overwrite local files", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE ) == JOptionPane.YES_OPTION ) { skipFiles.clear(); } } return skipFiles; } private static void doPush( File file, String relpath ) { File rebase = getRebase( relpath ); if ( rebase == null ) // this is okay; just make the file in its default location rebase = new File( KoLConstants.ROOT_LOCATION, relpath ); rebase.getParentFile().mkdirs(); RequestLogger.printLine( file.getName() + " => " + rebase.getPath() ); FileUtilities.copyFile( file, rebase ); } private static void doDelete( File file, String relpath ) { File rebase = getRebase( relpath ); if ( rebase == null ) return; if ( rebase.exists() ) { String rerebase = FileUtilities.getRelativePath( KoLConstants.ROOT_LOCATION , rebase ); if ( rebase.delete() ) RequestLogger.printLine( rerebase + " => DELETED" ); } } private static boolean shouldPush( SVNFileEvent event, boolean wasCheckout, String relpath ) { if ( wasCheckout ) return true; if ( event.getEvent().getAction() == SVNEventAction.UPDATE_ADD ) // new text file added to repo, user said it was okay return true; if ( event.getEvent().getAction() == SVNEventAction.ADD ) // new binary file added to repo, user said it was okay return true; if ( event.getEvent().getAction() == SVNEventAction.UPDATE_UPDATE ) { SVNStatusType status = event.getEvent().getContentsStatus(); // only push updated files if the file still exists in the mafia subdirectory if ( status == SVNStatusType.MERGED ) return rebaseExists( relpath ); if ( status == SVNStatusType.CHANGED ) return rebaseExists( relpath ); if ( status == SVNStatusType.CONFLICTED ) return false; } // deletion handling is separate from the "push" handling if ( event.getEvent().getAction() == SVNEventAction.UPDATE_DELETE ) return false; // probably shouldn't get here. RequestLogger.printLine( "unhandled SVN event: " + event.getEvent() + "; please report this." ); return false; } private static boolean shouldDelete( SVNFileEvent event ) { if ( event.getEvent().getAction() == SVNEventAction.UPDATE_DELETE ) return true; return false; } private static boolean rebaseExists( String relpath ) { File rebase = new File( KoLConstants.ROOT_LOCATION, relpath ); List<File> matches = KoLmafiaCLI.findScriptFile( rebase.getName() ); // data/ is not in the searchable namespace for findScriptFile, but we need it to be. // getRebase will still find it if it exists in the default location, so look for that. if ( relpath.startsWith( "data" ) ) { if ( rebase.exists() ) matches.add( rebase ); } if ( matches.size() > 1 ) { RequestLogger.printLine( "WARNING: too many matches for " + rebase.getName() + " in your namespace; no local files were updated." ); } if ( matches.size() == 0 ) { RequestLogger.printLine( "NOTE: no local file named " + rebase.getName() + " in your namespace; no updates performed for this file." ); } return matches.size() == 1; } /** * We need to reproducibly format a unique identifier for a given project; there can be multiple projects for a * given repo. * <p> * We hardcode a regex to handle sourceforge (in future, possibly other) URLS. We want to turn * https://svn.code.sf.net/p/mafiasvntest/code/myvalidproject1/ into "mafiasvntest-myvalidproject1". Likewise * https://svn.code.sf.net/p/mafiasvntest/code/trunk/branchA/myvalidproject1/ becomes * "mafiasvntest-trunk-branchA-myvalidproject1". * <p> * If the regex fails to match, we fall back and get the SVN repo UUID. This means that checking out multiple * projects will fail (since we can't put multiple working copies in one directory). Still, better than nothing. * * @param repo the repo to get a unique folder name for * @return a unique folder ID for a given repo URL */ static String getFolderUUID( SVNURL repo ) { String remote = null; // first, make sure the repo is there. try { SVN_LOCK.lock(); remote = getClientManager().createRepository( repo, true ).getRepositoryUUID( false ); } catch ( SVNException e ) { error( e, "Unable to connect with repository at " + repo.getPath() ); return null; } finally { SVN_LOCK.unlock(); } String local = getFolderUUIDNoRemote( repo ); return local != null ? local : remote; } public static String getFolderUUIDNoRemote( SVNURL repo ) { String UUID = null; Matcher m; if ( ( m = SVNManager.SOURCEFORGE_PATTERN.matcher( repo.getPath() ) ).find() ) { // replace awful SVN UUID with nicely-formatted string derived from URL UUID = StringUtilities.globalStringReplace( m.group( 1 ) + m.group( 2 ), "/", "-" );// } else if ( ( m = GOOGLECODE_HOST_PATTERN.matcher( repo.getHost() ) ).find() ) { UUID = m.group( 1 ) + StringUtilities.globalStringReplace( repo.getPath().substring( 4 ), "/", "-" ); } else if ( repo.getHost().contains( "github" ) ) { UUID = StringUtilities.globalStringReplace( repo.getPath().substring( 1 ), "/", "-" ); } return UUID; } static File doDirSetup( String uuid ) { File makeDir = new File( KoLConstants.SVN_LOCATION, uuid ); if ( !makeDir.mkdirs() // if we successfully make the directory, ok && !makeDir.exists() ) // if it already exists, ok return null; // else punt return makeDir; } /** * Accessory method to queue up a local file copy event. * * @param event */ public static void queueFileEvent( SVNFileEvent event ) { SVNManager.eventStack.add( event ); } public static void doUpdate() { final File[] projects = KoLConstants.SVN_LOCATION.listFiles(); if ( projects == null || projects.length == 0 ) { RequestLogger.printLine( "No projects currently installed with SVN." ); return; } initialize(); Runnable runMe = new Runnable() { public void run() { KoLmafia.updateDisplay( "Checking all SVN projects..." ); List<File> projectsToUpdate = new ArrayList<File>(); List<CheckStatusRunnable> checkingRunnables = new ArrayList<CheckStatusRunnable>(); // checking projects should be parallelized; it is a read-only operation, which is thread-safe for ( File f : projects ) { if ( !KoLmafia.permitsContinue() ) { return; } // skip hidden files; OSX tends to pepper your filesystem with them, apparently if ( f.getName().startsWith( "." ) ) { continue; } WCAtHead( f, false, checkingRunnables ); } if (!checkingRunnables.isEmpty()) { // now start all threads and wait for them to finish. // we must keep one big lock over the entire time, until no thread accesses it anymore try { Map<Thread, CheckStatusRunnable> trMap = new HashMap<Thread, CheckStatusRunnable>(); SVN_LOCK.lock(); for (CheckStatusRunnable r : checkingRunnables) { Thread t = new Thread( r ); trMap.put(t, r); t.start(); } for (Thread t : trMap.keySet()) { CheckStatusRunnable r = trMap.get(t); try { t.join(); } catch (InterruptedException ie) { r.reportInterrupt(); } if (r.shouldBeUpdated()) { projectsToUpdate.add(r.getOriginalFile()); } } } finally { SVN_LOCK.unlock(); } } // updates are serialized, if there even are any KoLmafia.updateDisplay( "Updating all SVN projects..." ); for ( File f : projectsToUpdate ) { if ( !KoLmafia.permitsContinue() ) { return; } RequestThread.postRequest( new UpdateRunnable( f ) ); pushUpdates(); } } }; if ( SVN_LOCK.tryLock() ) { try { RequestThread.postRequest( runMe ); showCommitMessages(); Preferences.setBoolean( "_svnUpdated", true ); } finally { SVN_LOCK.unlock(); } } else { RequestLogger.printLine( "SVN busy; update operation cancelled." ); return; } if ( Preferences.getBoolean( "syncAfterSvnUpdate" ) ) { syncAll(); } if ( Preferences.getBoolean( "svnInstallDependencies" ) ) checkDependencies(); } public static boolean WCAtHead( File f, boolean quiet ) { return WCAtHead( f, quiet, null ); } /** * For users who just want "simple" update behavior, check if the revision of the project root and the repo root are * the same. * <p> * Users who have used <code>svn switch</code> on some of their project should not use this. * * @param f the working copy * @param quiet if <code>true</code>, suppresses RequestLogger output. * @param runnables if present, enables multi-threaded behaviour, and adds a runnable to the list; otherwise executes the runnable * @return <code>true</code> if the working copy is at HEAD, or runnable was successfully added to <code>runnables</code> */ public static boolean WCAtHead( File f, boolean quiet, List<CheckStatusRunnable> runnables ) { boolean mayReuseRepo = runnables == null; try { SVN_LOCK.lock(); if ( !SVNWCUtil.isWorkingCopyRoot( f ) ) { return false; } SVNInfo wcinfo = getClientManager().getWCClient().doInfo( f, SVNRevision.WORKING ); SVNRepository repo = getClientManager().createRepository( wcinfo.getURL(), mayReuseRepo ); long wcRevisionNumber = wcinfo.getRevision().getNumber(); CheckStatusRunnable checkStatusRunnable = new CheckStatusRunnable( f, repo, wcRevisionNumber, wcinfo.getFile(), quiet ); if (runnables != null) { runnables.add(checkStatusRunnable); } else { checkStatusRunnable.run(); if ( checkStatusRunnable.isAtHead() ) { return true; } } } catch ( SVNException e ) { error( e ); return true; // don't continue updating this project if there's an error here. } finally { SVN_LOCK.unlock(); } return false; } /** * Performs an <code>svn update</code> on a local working copy. * * @param p the local working copy to update. */ public static void doUpdate( String p ) { File project = new File( KoLConstants.SVN_LOCATION, p ); if ( !project.exists() ) return; initialize(); if ( SVN_LOCK.tryLock() ) // if we're locked, bounce requests to update an individual script immediately { try { RequestThread.postRequest( new UpdateRunnable( project ) ); pushUpdates(); showCommitMessages(); } finally { SVN_LOCK.unlock(); } } else { //RequestLogger.printLine( "SVN busy; duplicate update operation cancelled." ); return; } if ( Preferences.getBoolean( "svnInstallDependencies" ) ) checkDependencies(); } /** * Performs an <code>svn update</code> on one individual repo. * * @param repo the <b>SVNURL</b> to update. */ public static void doUpdate( SVNURL repo ) { initialize(); RequestThread.postRequest( new UpdateRunnable( repo ) );// pushUpdates(); showCommitMessages(); if ( Preferences.getBoolean( "svnInstallDependencies" ) ) checkDependencies(); } public static void deleteInstalledProject( String p ) { final File project = new File( KoLConstants.SVN_LOCATION, p ); if ( !project.exists() ) { return; } deleteInstalledProject( project ); } public static void deleteInstalledProject( final File project ) { RequestLogger.printLine( "Uninstalling project..." ); recursiveDelete( project ); if ( project.exists() ) { // sometimes SVN daemon threads (like tsvncache) will have the lock for wc.db, causing delete to fail for that file (and therefore also the project directory). // dispatch a parallel thread that will wait for a little bit then re-try the delete. RequestThread.runInParallel( new Runnable() { public void run() { PauseObject p = new PauseObject(); p.pause( 2000 ); recursiveDelete( project ); } }); } RequestLogger.printLine( "Project uninstalled." ); } private static void recursiveDelete( File f ) { if ( f.isDirectory() ) { for ( File c : f.listFiles() ) recursiveDelete( c ); } // findDepth doesn't know how to find the depth of the project folder itself, so don't try. We don't need to rebase-delete it anyway. if ( !f.getParentFile().equals( KoLConstants.SVN_LOCATION ) ) { File fDepth = findDepth( f, true ); if ( fDepth == null ) { return; } String relpath = FileUtilities.getRelativePath( fDepth.getParentFile(), f ); if ( !relpath.startsWith( "." ) ) // do not try to delete the rebase of hidden folders such as .svn! { File rebase = getRebase( relpath ); if ( rebase != null ) { String rerebase = FileUtilities.getRelativePath( KoLConstants.ROOT_LOCATION , rebase ); if ( rebase.delete() ) RequestLogger.printLine( rerebase + " => DELETED" ); } } } f.delete(); } /** * Move the working copy up or down <b>amount</b> revisions. <b>amount</b> can be negative. * * @param p the name of the project to decrement * @param amount the amount to increment/decrement */ public static void incrementProject( String p, int amount ) { if ( amount == 0 ) return; File project = new File( KoLConstants.SVN_LOCATION, p ); if ( !project.exists() ) return; initialize(); try { SVN_LOCK.lock(); long currentRev = getClientManager().getStatusClient().doStatus( project, false ).getRevision().getNumber(); if ( currentRev + amount <= 0 ) { RequestLogger.printLine( "At r" + currentRev + "; cannot decrement revision by " + amount + "." ); return; } RequestLogger.printLine( ( ( amount > 0 ) ? "Incrementing" : "Decrementing" ) + " project " + project.getName() + " from r" + currentRev + " to r" + ( currentRev + amount ) ); SVNManager.update( project, SVNRevision.create( currentRev + amount ), true ); } catch ( SVNException e ) { if ( e.getErrorMessage().getErrorCode().equals( SVNErrorCode.FS_NO_SUCH_REVISION ) ) { RequestLogger.printLine( "SVN Error: no such revision. Aborting..." ); return; } error( e, "SVN ERROR during update operation. Aborting..." ); return; } finally { SVN_LOCK.unlock(); } pushUpdates(); showCommitMessages(); } /** * "sync" operations are for users who make edits to the working copy version of files (in svn/) and want those * changes reflected in their local copy. * <p> * Sync first iterates through projects and builds a list of working copy files that are modified. * <p> * For files that are modified, it then compares the WC file against the local rebase. If the rebase is different, * the WC file is copied over the rebase. */ public static void syncAll() { if ( !KoLmafia.permitsContinue() ) return; File[] projects = KoLConstants.SVN_LOCATION.listFiles(); if ( projects == null || projects.length == 0 ) { // Nothing to do here. return; } initialize(); RequestLogger.printLine( "Checking for working copy modifications..." ); for ( File f : projects ) { try { SVN_LOCK.lock(); getClientManager().getStatusClient().doStatus( f, SVNRevision.UNDEFINED, SVNDepth.INFINITY, false, false, false, false, new StatusHandler(), null ); } catch ( SVNException e ) { error( e ); return; } finally { SVN_LOCK.unlock(); } } if ( eventStack.isEmpty() ) { // nothing to do RequestLogger.printLine( "No modifications." ); return; } RequestLogger.printLine( "Synchronizing with local copies..." ); while ( !eventStack.isEmpty() ) { File eventFile = eventStack.pop().getFile(); if ( eventFile.isDirectory() ) { continue; // directories are harmless } if ( eventFile.getParentFile().getParentFile().equals( KoLConstants.SVN_LOCATION ) ) { continue; // no top-level files get pushed - including dependencies.txt } File fDepth = findDepth( eventFile.getParentFile() ); if ( fDepth == null ) { //shouldn't happen, punt eventStack.clear(); return; } String relpath = FileUtilities.getRelativePath( fDepth.getParentFile(), eventFile ); File rebase = getRebase( relpath ); if ( rebase == null ) continue; try { SVN_LOCK.lock(); if ( rebaseExists( relpath ) && !SVNFileUtil.compareFiles( eventFile, rebase, null ) ) { doPush( eventFile, relpath ); } } catch ( SVNException e ) { error( e ); eventStack.clear(); return; } finally { SVN_LOCK.unlock(); } } RequestLogger.printLine( "Sync complete." ); } private static File getRebase( String relpath ) { File rebase = new File( KoLConstants.ROOT_LOCATION, relpath ); // scripts/ and relay/ exist in the searchable namespace, so only search if we're looking there // the root location is also in the namespace, but svn isn't allowed to put stuff there, so ignore it if ( relpath.startsWith( "scripts" ) || relpath.startsWith( "relay" ) ) { List<File> matches = KoLmafiaCLI.findScriptFile( rebase.getName() ); if ( matches.size() == 1 ) return matches.get( 0 ); if ( matches.size() > 1 ) return null; } // some directories are not searched by findScriptFile, but we can just check if the rebase exists in those cases if ( rebase.exists() ) return rebase; return null; } private static void checkDependencies() { checkDependencies( 0 ); } private static void checkDependencies( int recursionDepth ) { if ( !KoLmafia.permitsContinue() ) return; File[] projects = KoLConstants.SVN_LOCATION.listFiles(); List<File> dependencyFiles = new ArrayList<File>(); if ( projects == null || projects.length == 0 ) { // Nothing to do here. return; } for ( File f : projects ) { File dep = new File( f, "dependencies.txt" ); if ( dep.exists() ) dependencyFiles.add( dep ); } if ( dependencyFiles.size() == 0 ) return; // we have some dependencies to resolve. We need to figure out what SVNURLs are already installed. initialize(); Set<String> installed = new HashSet<String>(); for ( File f : projects ) { try { String uuid = getFolderUUIDNoRemote( SVNManager.getClientManager().getStatusClient().doStatus( f, false ).getURL() ); if ( uuid == null ) uuid = getFolderUUID( SVNManager.getClientManager().getStatusClient().doStatus( f, false ).getURL() ); installed.add( uuid ); } catch ( SVNException e ) { // shouldn't happen, punt error( e ); return; } } // Now we need to figure out the set of URLs specified by dependency files Set<SVNURL> dependencyURLs = new HashSet<SVNURL>(); for ( File dep : dependencyFiles ) { dependencyURLs.addAll( readDependencies( dep ) ); } if ( dependencyURLs.size() == 0 ) return; // now, see if there are any files in dependencyURLs that aren't yet installed Set<SVNURL> installMe = new HashSet<SVNURL>(); for ( SVNURL url : dependencyURLs ) { if ( url == null ) continue; // to figure out if they are installed, compare what the UUID would be of both. // convert each dependencyURL to a UUID before comparing. String uuid = getFolderUUIDNoRemote( url ); if ( uuid == null ) uuid = getFolderUUID( url ); if ( !installed.contains( uuid ) ) installMe.add( url ); } if ( installMe.size() == 0 ) return; // before installing, we need to check that they're actually valid Iterator<SVNURL> it = installMe.iterator(); while ( it.hasNext() ) { SVNURL url = it.next(); if ( validateRepo( url, true ) ) { RequestLogger.printLine( "bogus dependency: " + url ); it.remove(); } } if ( installMe.size() == 0 ) return; if ( !KoLmafia.permitsContinue() ) return; // install them RequestLogger.printLine( "Installing " + installMe.size() + " new dependenc" + ( installMe.size() == 1 ? "y." : "ies." ) ); for ( SVNURL url : installMe ) { RequestThread.postRequest( new CheckoutRunnable( url ) ); pushUpdates( true ); } if ( recursionDepth <= DEPENDENCY_RECURSION_LIMIT ) { checkDependencies( ++recursionDepth ); } else { RequestLogger.printLine( "Stopping dependency installation: Too Much Recursion. Doing svn update will continue to resolve them if you want."); } } /** * Reads a dependencies.txt text file and pulls out the URLs. * <p> * Output should be sanitized, with comments, bogus lines, and duplicate entries (obviously, since it's a set) * removed. * * @param dep the <code>File</code> that contains the dependencies * @return a <code>Set</code> of <code>SVNURL</code>s that are dependencies */ private static Set<SVNURL> readDependencies( File dep ) { BufferedReader reader = FileUtilities.getReader( dep ); Set<SVNURL> depURLs = new HashSet<SVNURL>(); try { String[] data; while ( ( data = FileUtilities.readData( reader ) ) != null ) { // turn it into an SVNURL try { depURLs.add( SVNURL.parseURIEncoded( data[ 0 ] ) ); } catch ( SVNException e ) { RequestLogger.printLine( "Bad line of data in " + dep + "; skipping this file." ); depURLs.clear(); break; } } } finally { try { reader.close(); } catch ( Exception e ) { StaticEntity.printStackTrace( e ); } } return depURLs; } private static void error( SVNException e ) { error( e, null ); } public static void error( SVNException e, String addMessage ) { RequestLogger.printLine( e.getErrorMessage().getMessage() ); if ( addMessage != null ) KoLmafia.updateDisplay( MafiaState.ERROR, addMessage ); } static SVNClientManager getClientManager() { if ( ourClientManager == null ) { setupLibrary(); } return ourClientManager; } public static SVNURL workingCopyToSVNURL( File WCDir ) throws SVNException { return SVNManager.getClientManager().getStatusClient().doStatus( WCDir, false ).getURL(); } // some functions taken/adapted from http://wiki.svnkit.com/Managing_A_Working_Copy // there are a number of other examples there. }