/*
* ====================================================================
* 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 org.tmatesoft.svn.core.wc.admin;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLock;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.fs.FSCommitter;
import org.tmatesoft.svn.core.internal.io.fs.FSFS;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryUtil;
import org.tmatesoft.svn.core.internal.io.fs.FSRevisionRoot;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNUUIDGenerator;
import org.tmatesoft.svn.core.internal.wc.DefaultLoadHandler;
import org.tmatesoft.svn.core.internal.wc.ISVNLoadHandler;
import org.tmatesoft.svn.core.internal.wc.SVNAdminHelper;
import org.tmatesoft.svn.core.internal.wc.SVNCancellableEditor;
import org.tmatesoft.svn.core.internal.wc.SVNDumpEditor;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNSynchronizeEditor;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.ISVNLockHandler;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.replicator.SVNRepositoryReplicator;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNRepositoryPool;
import org.tmatesoft.svn.core.wc.SVNBasicClient;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNDebugLog;
/**
* The <b>SVNAdminClient</b> class provides methods that brings repository-side functionality
* and repository synchronizing features.
*
* <p>
* Repository administrative methods are analogues of the corresponding commands of the native
* Subversion 'svnadmin' utility, while repository synchronizing methods are the ones for the
* 'svnsync' utility.
*
* <p>
* Here's a list of the <b>SVNAdminClient</b>'s methods
* matched against corresponing commands of the Subversion svnsync and svnadmin command-line utilities:
*
* <table cellpadding="3" cellspacing="1" border="0" width="40%" bgcolor="#999933">
* <tr bgcolor="#ADB8D9" align="left">
* <td><b>SVNKit</b></td>
* <td><b>Subversion</b></td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doInitialize()</td><td>'svnsync initialize'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doSynchronize()</td><td>'svnsync synchronize'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doCopyRevisionProperties()</td><td>'svnsync copy-revprops'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doDump()</td><td>'svnadmin dump'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doListTransactions()</td><td>'svnadmin lstxns'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doLoad()</td><td>'svnadmin load'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doRemoveTransactions()</td><td>'svnadmin rmtxns'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doVerify()</td><td>'svnadmin verify'</td>
* </tr>
* </table>
*
* @version 1.1.1
* @author TMate Software Ltd.
* @since 1.1.0
*/
public class SVNAdminClient extends SVNBasicClient {
private ISVNLogEntryHandler mySyncHandler;
private ISVNLoadHandler myLoadHandler;
private ISVNAdminEventHandler myEventHandler;
/**
* Creates a new admin client.
*
* @param authManager an auth manager
* @param options an options driver
*/
public SVNAdminClient(ISVNAuthenticationManager authManager, ISVNOptions options) {
super(authManager, options);
}
/**
* Creates a new admin client.
*
* @param repositoryPool a repository pool
* @param options an options driver
*/
public SVNAdminClient(ISVNRepositoryPool repositoryPool, ISVNOptions options) {
super(repositoryPool, options);
}
/**
* Sets a replication handler that will receive a log entry object
* per each replayed revision.
*
* <p>
* Log entries dispatched to the handler may not contain changed paths and
* committed log message until this features are implemented in future releases.
*
* @param handler a replay handler
*/
public void setReplayHandler(ISVNLogEntryHandler handler) {
mySyncHandler = handler;
}
/**
* Sets an event handler for this object.
* {@link ISVNAdminEventHandler} should be provided to <b>SVNAdminClent</b>
* via this method also.
*
* @param handler an event handler
*/
public void setEventHandler(ISVNEventHandler handler) {
super.setEventHandler(handler);
if (handler instanceof ISVNAdminEventHandler) {
myEventHandler = (ISVNAdminEventHandler) handler;
}
}
/**
* Creates an FSFS-type repository.
*
* This implementation uses {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory#createLocalRepository(File, String, boolean, boolean)}}.
* <p>
* If <code>uuid</code> is <span class="javakeyword">null</span> a new uuid will be generated, otherwise
* the specified will be used.
*
* <p>
* If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>, an empty
* pre-revprop-change hook will be placed into the repository /hooks subdir. This enables changes to
* revision properties of the newly created repository.
*
* <p>
* If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
* exists, deletes that path and creates a repository in its place.
*
* @param path a repository root dir path
* @param uuid a repository uuid
* @param enableRevisionProperties enables/disables changes to revision properties
* @param force forces operation to run
* @return a local URL (file:///) of a newly created repository
* @throws SVNException
* @see #doCreateRepository(File, String, boolean, boolean, boolean)
* @since 1.1.0
*/
public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force) throws SVNException {
return SVNRepositoryFactory.createLocalRepository(path, uuid, enableRevisionProperties, force);
}
/**
* Creates an FSFS-type repository.
*
* This implementation uses {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory#createLocalRepository(File, String, boolean, boolean)}}.
* <p>
* If <code>uuid</code> is <span class="javakeyword">null</span> a new uuid will be generated, otherwise
* the specified will be used.
*
* <p>
* If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>, an empty
* pre-revprop-change hook will be placed into the repository /hooks subdir. This enables changes to
* revision properties of the newly created repository.
*
* <p>
* If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
* exists, deletes that path and creates a repository in its place.
*
* <p>
* Set <code>pre14Compatible</code> to <span class="javakeyword">true</span> if you want a new repository
* to be compatible with pre-1.4 servers.
*
* @param path a repository root dir path
* @param uuid a repository uuid
* @param enableRevisionProperties enables/disables changes to revision properties
* @param force forces operation to run
* @param pre14Compatible <span class="javakeyword">true</span> to
* create a repository with pre-1.4 format
* @return a local URL (file:///) of a newly created repository
* @throws SVNException
* @since 1.1.1
*/
public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force, boolean pre14Compatible) throws SVNException {
return SVNRepositoryFactory.createLocalRepository(path, uuid, enableRevisionProperties, force, pre14Compatible);
}
/**
* Copies revision properties from the source repository that the destination one is synchronized with
* to the given revision of the destination repository itself.
*
* <p>
* This method is equivalent to the command 'copy-revprops' of the native Subversion <i>svnsync</i> utility.
* Note that the destination repository given as <code>toURL</code> must be synchronized with a source
* repository. Please, see {@link #doInitialize(SVNURL, SVNURL)}} how to initialize such a synchronization.
*
* @param toURL a url to the destination repository which must be synchronized
* with another repository
* @param revision a particular revision of the source repository to copy revision properties
* from
* @throws SVNException
* @since 1.1, new in Subversion 1.4
*/
public void doCopyRevisionProperties(SVNURL toURL, long revision) throws SVNException {
SVNRepository toRepos = createRepository(toURL, true);
checkIfRepositoryIsAtRoot(toRepos, toURL);
SVNException error = null;
SVNException error2 = null;
lock(toRepos);
try {
SessionInfo info = openSourceRepository(toRepos);
if (revision > info.myLastMergedRevision) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot copy revprops for a revision that has not been synchronized yet");
SVNErrorManager.error(err);
}
copyRevisionProperties(info.myRepository, toRepos, revision, false);
} catch (SVNException svne) {
error = svne;
} finally {
try {
unlock(toRepos);
} catch (SVNException svne) {
error2 = svne;
}
}
if (error != null) {
throw error;
} else if (error2 != null) {
throw error2;
}
}
/**
* Initializes synchronization between source and target repositories.
*
* <p>
* This method is equivalent to the command 'initialize' ('init') of the native Subversion <i>svnsync</i>
* utility. Initialization places information of a source repository to a destination one (setting special
* revision properties in revision 0) as well as copies all revision props from revision 0 of the source
* repository to revision 0 of the destination one.
*
* @param fromURL a source repository url
* @param toURL a destination repository url
* @throws SVNException
* @since 1.1, new in Subversion 1.4
*/
public void doInitialize(SVNURL fromURL, SVNURL toURL) throws SVNException {
SVNRepository toRepos = createRepository(toURL, true);
checkIfRepositoryIsAtRoot(toRepos, toURL);
SVNException error = null;
SVNException error2 = null;
lock(toRepos);
try {
long latestRevision = toRepos.getLatestRevision();
if (latestRevision != 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot initialize a repository with content in it");
SVNErrorManager.error(err);
}
String fromURLProp = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL);
if (fromURLProp != null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Destination repository is already synchronizing from ''{0}''", fromURLProp);
SVNErrorManager.error(err);
}
// TODO close session.
SVNRepository fromRepos = createRepository(fromURL, false);
checkIfRepositoryIsAtRoot(fromRepos, fromURL);
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL, fromURL.toDecodedString());
String uuid = fromRepos.getRepositoryUUID(true);
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID, uuid);
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION, "0");
copyRevisionProperties(fromRepos, toRepos, 0, false);
} catch (SVNException svne) {
error = svne;
} finally {
try {
unlock(toRepos);
} catch (SVNException svne) {
error2 = svne;
}
}
if (error != null) {
throw error;
} else if (error2 != null) {
throw error2;
}
}
/**
* Completely synchronizes two repositories.
*
* <p>
* This method initializes the destination repository and then copies all revision
* changes (including revision properties)
* from the given source repository to the destination one. First it
* tries to use synchronization features similar to the native Subversion
* 'svnsync' capabilities. But if a server does not support
* <code>replay</code> functionality, SVNKit uses its own repository
* replication feature (see {@link org.tmatesoft.svn.core.replicator.SVNRepositoryReplicator}})
*
* @param fromURL a url of a repository to copy from
* @param toURL a destination repository url
* @throws SVNException
* @since 1.1
*/
public void doCompleteSynchronize(SVNURL fromURL, SVNURL toURL) throws SVNException {
try {
doInitialize(fromURL, toURL);
doSynchronize(toURL);
return;
} catch (SVNException svne) {
if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.RA_NOT_IMPLEMENTED) {
throw svne;
}
}
SVNRepositoryReplicator replicator = SVNRepositoryReplicator.newInstance();
SVNRepository fromRepos = createRepository(fromURL, true);
// TODO close session
SVNRepository toRepos = createRepository(toURL, false);
replicator.replicateRepository(fromRepos, toRepos, 1, -1);
}
/**
* Synchronizes the repository at the given url.
*
* <p>
* Synchronization means copying revision changes and revision properties from the source
* repository (that the destination one is synchronized with) to the destination one starting at
* the last merged revision. This method is equivalent to the command 'synchronize' ('sync') of
* the native Subversion <i>svnsync</i> utility.
*
* @param toURL a destination repository url
* @throws SVNException
* @since 1.1, new in Subversion 1.4
*/
public void doSynchronize(SVNURL toURL) throws SVNException {
SVNRepository toRepos = createRepository(toURL, true);
checkIfRepositoryIsAtRoot(toRepos, toURL);
SVNException error = null;
SVNException error2 = null;
lock(toRepos);
try {
SessionInfo info = openSourceRepository(toRepos);
SVNRepository fromRepos = info.myRepository;
long lastMergedRevision = info.myLastMergedRevision;
String currentlyCopying = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.CURRENTLY_COPYING);
long toLatestRevision = toRepos.getLatestRevision();
if (currentlyCopying != null) {
long copyingRev = Long.parseLong(currentlyCopying);
if (copyingRev < lastMergedRevision || copyingRev > lastMergedRevision + 1 || (toLatestRevision != lastMergedRevision && toLatestRevision != copyingRev)) {
SVNErrorMessage err = SVNErrorMessage
.create(
SVNErrorCode.IO_ERROR,
"Revision being currently copied ({0,number,integer}), last merged revision ({1,number,integer}), and destination HEAD ({2,number,integer}) are inconsistent; have you committed to the destination without using svnsync?",
new Long[] {
new Long(copyingRev), new Long(lastMergedRevision), new Long(toLatestRevision)
});
SVNErrorManager.error(err);
} else if (copyingRev == toLatestRevision) {
if (copyingRev > lastMergedRevision) {
copyRevisionProperties(fromRepos, toRepos, toLatestRevision, true);
lastMergedRevision = copyingRev;
}
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION, SVNProperty.toString(lastMergedRevision));
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.CURRENTLY_COPYING, null);
}
} else {
if (toLatestRevision != lastMergedRevision) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Destination HEAD ({0,number,integer}) is not the last merged revision ({1,number,integer}); have you committed to the destination without using svnsync?", new Long[] {new Long(toLatestRevision), new Long(lastMergedRevision)});
SVNErrorManager.error(err);
}
}
long fromLatestRevision = fromRepos.getLatestRevision();
if (fromLatestRevision < lastMergedRevision) {
return;
}
for (long currentRev = lastMergedRevision + 1; currentRev <= fromLatestRevision; currentRev++) {
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.CURRENTLY_COPYING, SVNProperty.toString(currentRev));
SVNSynchronizeEditor syncEditor = new SVNSynchronizeEditor(toRepos, mySyncHandler, currentRev - 1);
ISVNEditor cancellableEditor = SVNCancellableEditor.newInstance(syncEditor, this, getDebugLog());
try {
fromRepos.replay(0, currentRev, true, cancellableEditor);
} catch (SVNException e) {
try {
cancellableEditor.abortEdit();
} catch (SVNException abortError) {}
throw e;
}
cancellableEditor.closeEdit();
if (syncEditor.getCommitInfo().getNewRevision() != currentRev) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Commit created rev {0,number,integer} but should have created {1,number,integer}", new Long[] {
new Long(syncEditor.getCommitInfo().getNewRevision()), new Long(currentRev)
});
SVNErrorManager.error(err);
}
copyRevisionProperties(fromRepos, toRepos, currentRev, true);
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION, SVNProperty.toString(currentRev));
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.CURRENTLY_COPYING, null);
}
} catch (SVNException svne) {
error = svne;
} finally {
try {
unlock(toRepos);
} catch (SVNException svne) {
error2 = svne;
}
}
if (error != null) {
throw error;
} else if (error2 != null) {
throw error2;
}
}
public void doListLocks(File repositoryRoot) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot);
File digestFile = fsfs.getDigestFileFromRepositoryPath("/");
ISVNLockHandler handler = new ISVNLockHandler() {
public void handleLock(String path, SVNLock lock, SVNErrorMessage error) throws SVNException {
checkCancelled();
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.LOCK_LISTED, lock, error, null);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
public void handleUnlock(String path, SVNLock lock, SVNErrorMessage error) throws SVNException {
}
};
fsfs.walkDigestFiles(digestFile, handler, false);
}
public void doRemoveLocks(File repositoryRoot, String[] paths) throws SVNException {
if (paths == null) {
return;
}
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot);
for (int i = 0; i < paths.length; i++) {
String path = paths[i];
if (path == null) {
continue;
}
checkCancelled();
SVNLock lock = null;
try {
lock = fsfs.getLockHelper(path, false);
if (lock == null) {
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.NOT_LOCKED, lock, null, "Path '" + path + "' isn't locked.");
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
continue;
}
fsfs.unlockPath(path, lock.getID(), null, true, false);
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.UNLOCKED, lock, null, "Removed lock on '" + path + "'.");
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
} catch (SVNException svne) {
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.UNLOCK_FAILED, lock, svne.getErrorMessage(), "svnadmin: " + svne.getErrorMessage().getFullMessage());
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
}
}
/**
* Lists all uncommitted transactions.
* On each uncommetted transaction found this method fires an {@link SVNAdminEvent}
* with action set to {@link SVNAdminEventAction#TRANSACTION_LISTED} to the registered
* {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
* pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
* information can be retrieved out of {@link SVNAdminEvent}:
* <ul>
* <li>transaction name - use {@link SVNAdminEvent#getTxnName() SVNAdminEvent.getTxnName()} to get it</li>
* <li>transaction directory - use {@link SVNAdminEvent#getTxnDir() SVNAdminEvent.getTxnDir()} to get it</li>
* </ul>
*
* @param repositoryRoot a repository root directory path
* @throws SVNException
* @since 1.1.1
*/
public void doListTransactions(File repositoryRoot) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot);
Map txns = fsfs.listTransactions();
for(Iterator names = txns.keySet().iterator(); names.hasNext();) {
String txnName = (String) names.next();
File txnDir = (File) txns.get(txnName);
SVNDebugLog.getDefaultLog().info(txnName + "\n");
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(txnName, txnDir, SVNAdminEventAction.TRANSACTION_LISTED);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
}
/**
* Removes the specified outstanding transactions from a repository.
* On each transaction removed this method fires an {@link SVNAdminEvent}
* with action set to {@link SVNAdminEventAction#TRANSACTION_REMOVED} to the registered
* {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
* pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
* information can be retrieved out of {@link SVNAdminEvent}:
* <ul>
* <li>transaction name - use {@link SVNAdminEvent#getTxnName() SVNAdminEvent.getTxnName()} to get it</li>
* <li>transaction directory - use {@link SVNAdminEvent#getTxnDir() SVNAdminEvent.getTxnDir()} to get it</li>
* </ul>
*
* @param repositoryRoot a repository root directory path
* @param transactions an array with transaction names
* @throws SVNException
* @since 1.1.1
*/
public void doRemoveTransactions(File repositoryRoot, String[] transactions) throws SVNException {
if (transactions == null) {
return;
}
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot);
for (int i = 0; i < transactions.length; i++) {
String txnName = transactions[i];
fsfs.openTxn(txnName);
FSCommitter.purgeTxn(fsfs, txnName);
SVNDebugLog.getDefaultLog().info("Transaction '" + txnName + "' removed.\n");
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(txnName, fsfs.getTransactionDir(txnName), SVNAdminEventAction.TRANSACTION_REMOVED);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
}
/**
* Verifies the data stored in the repository. This method uses the dump implementation
* (non incremental, beginning with revision 0, ending at the latest one)
* passing a dummy output stream to it. This allows to check the integrity of the
* repository data.
*
* @param repositoryRoot a repository root directory path
* @throws SVNException verification failed - a repository may be corrupted
* @since 1.1.1
*/
public void doVerify(File repositoryRoot) throws SVNException {
doVerify(repositoryRoot, SVNRevision.create(0), SVNRevision.HEAD);
}
public void doVerify(File repositoryRoot, SVNRevision startRevision, SVNRevision endRevision) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot);
long startRev = startRevision.getNumber();
long endRev = endRevision.getNumber();
if (startRev < 0) {
startRev = 0;
}
if (endRev < 0) {
endRev = fsfs.getYoungestRevision();
}
try {
dump(fsfs, SVNFileUtil.DUMMY_OUT, startRev, endRev, false, false);
} catch (IOException ioe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
SVNErrorManager.error(err, ioe);
}
}
/**
* Dumps contents of the repository to the provided output stream in a
* 'dumpfile' portable format.
*
* <p>
* On each revision dumped this method fires an {@link SVNAdminEvent}
* with action set to {@link SVNAdminEventAction#REVISION_DUMPED} to the registered
* {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
* pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
* information can be retrieved out of {@link SVNAdminEvent}:
* <ul>
* <li>dumped revision - use {@link SVNAdminEvent#getRevision() SVNAdminEvent.getRevision()} to get it</li>
* </ul>
*
* @param repositoryRoot a repository root directory path
* @param dumpStream an output stream to write dumped contents to
* @param startRevision the first revision to start dumping from
* @param endRevision the last revision to end dumping at
* @param isIncremental if <span class="javakeyword">true</span>
* then the first revision dumped will be a
* diff against the previous revision; otherwise
* the first revision is a fulltext.
* @param useDeltas if <span class="javakeyword">true</span>
* deltas will be written instead of fulltexts
* @throws SVNException
* @since 1.1.1
*/
public void doDump(File repositoryRoot, OutputStream dumpStream, SVNRevision startRevision, SVNRevision endRevision, boolean isIncremental, boolean useDeltas) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot);
long youngestRevision = fsfs.getYoungestRevision();
long lowerR = SVNAdminHelper.getRevisionNumber(startRevision, youngestRevision, fsfs);
long upperR = SVNAdminHelper.getRevisionNumber(endRevision, youngestRevision, fsfs);
if (!SVNRevision.isValidRevisionNumber(lowerR)) {
lowerR = 0;
upperR = youngestRevision;
} else if (!SVNRevision.isValidRevisionNumber(upperR)) {
upperR = lowerR;
}
if (lowerR > upperR) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CL_ARG_PARSING_ERROR, "First revision cannot be higher than second");
SVNErrorManager.error(err);
}
try {
dump(fsfs, dumpStream, lowerR, upperR, isIncremental, useDeltas);
} catch (IOException ioe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
SVNErrorManager.error(err, ioe);
}
}
/**
* Reads the provided dump stream committing new revisions to a repository.
*
* <p>
* On each revision loaded this method fires an {@link SVNAdminEvent}
* with action set to {@link SVNAdminEventAction#REVISION_LOADED} to the registered
* {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
* pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
* information can be retrieved out of {@link SVNAdminEvent}:
* <ul>
* <li>original revision - use {@link SVNAdminEvent#getOriginalRevision() SVNAdminEvent.getOriginalRevision()} to get it</li>
* <li>new committed revision - use {@link SVNAdminEvent#getRevision() SVNAdminEvent.getRevision()} to get it</li>
* </ul>
*
* <p>
* A call to this method is equivalent to
* <code>doLoad(repositoryRoot, dumpStream, false, false, SVNUUIDAction.DEFAULT, null)</code>.
*
* @param repositoryRoot the root directory path of the repository where
* new revisions will be committed
* @param dumpStream stream with dumped contents of a repository
* @throws SVNException
* @see #doLoad(File, InputStream, boolean, boolean, SVNUUIDAction, String)
* @since 1.1.1
*/
public void doLoad(File repositoryRoot, InputStream dumpStream) throws SVNException {
doLoad(repositoryRoot, dumpStream, false, false, SVNUUIDAction.DEFAULT, null);
}
/**
* Reads the provided dump stream committing new revisions to a repository.
*
* <p>
* On each revision loaded this method fires an {@link SVNAdminEvent}
* with action set to {@link SVNAdminEventAction#REVISION_LOADED} to the registered
* {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
* pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
* information can be retrieved out of {@link SVNAdminEvent}:
* <ul>
* <li>original revision - use {@link SVNAdminEvent#getOriginalRevision() SVNAdminEvent.getOriginalRevision()} to get it</li>
* <li>new committed revision - use {@link SVNAdminEvent#getRevision() SVNAdminEvent.getRevision()} to get it</li>
* </ul>
*
* @param repositoryRoot the root directory path of the repository where
* new revisions will be committed
* @param dumpStream stream with dumped contents of a repository
* @param usePreCommitHook if <span class="javakeyword">true</span>
* then calls a pre-commit hook before committing
* @param usePostCommitHook if <span class="javakeyword">true</span>
* then calls a post-commit hook after committing
* @param uuidAction one of the three possible ways to treat uuids
* @param parentDir if not <span class="javakeyword">null</span>
* then loads at this directory in the repository
* @throws SVNException
* @since 1.1.1
*/
public void doLoad(File repositoryRoot, InputStream dumpStream, boolean usePreCommitHook, boolean usePostCommitHook, SVNUUIDAction uuidAction, String parentDir) throws SVNException {
CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
ISVNLoadHandler handler = getLoadHandler(repositoryRoot, usePreCommitHook, usePostCommitHook, uuidAction, parentDir, decoder);
String line = null;
int version = -1;
StringBuffer buffer = new StringBuffer();
try {
line = SVNFileUtil.readLineFromStream(dumpStream, buffer, decoder);
if (line == null) {
SVNAdminHelper.generateIncompleteDataError();
}
//parse format
if (!line.startsWith(SVNAdminHelper.DUMPFILE_MAGIC_HEADER + ":")) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header");
SVNErrorManager.error(err);
}
try {
line = line.substring(SVNAdminHelper.DUMPFILE_MAGIC_HEADER.length() + 1);
line = line.trim();
version = Integer.parseInt(line);
if (version > SVNAdminHelper.DUMPFILE_FORMAT_VERSION) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Unsupported dumpfile version: {0,number,integer}", new Integer(version));
SVNErrorManager.error(err);
}
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header");
SVNErrorManager.error(err, nfe);
}
while (true) {
checkCancelled();
boolean foundNode = false;
//skip empty lines
buffer.setLength(0);
line = SVNFileUtil.readLineFromStream(dumpStream, buffer, decoder);
if (line == null) {
if (buffer.length() > 0) {
SVNAdminHelper.generateIncompleteDataError();
} else {
break;
}
}
if (line.length() == 0 || Character.isWhitespace(line.charAt(0))) {
continue;
}
Map headers = readHeaderBlock(dumpStream, line, decoder);
if (headers.containsKey(SVNAdminHelper.DUMPFILE_REVISION_NUMBER)) {
handler.closeRevision();
handler.openRevision(headers);
} else if (headers.containsKey(SVNAdminHelper.DUMPFILE_NODE_PATH)) {
handler.openNode(headers);
foundNode = true;
} else if (headers.containsKey(SVNAdminHelper.DUMPFILE_UUID)) {
String uuid = (String) headers.get(SVNAdminHelper.DUMPFILE_UUID);
handler.parseUUID(uuid);
} else if (headers.containsKey(SVNAdminHelper.DUMPFILE_MAGIC_HEADER)) {
try {
version = Integer.parseInt((String) headers.get(SVNAdminHelper.DUMPFILE_MAGIC_HEADER));
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header");
SVNErrorManager.error(err, nfe);
}
} else {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Unrecognized record type in stream");
SVNErrorManager.error(err);
}
String contentLength = (String) headers.get(SVNAdminHelper.DUMPFILE_CONTENT_LENGTH);
String propContentLength = (String) headers.get(SVNAdminHelper.DUMPFILE_PROP_CONTENT_LENGTH);
String textContentLength = (String) headers.get(SVNAdminHelper.DUMPFILE_TEXT_CONTENT_LENGTH);
boolean isOldVersion = version == 1 && contentLength != null && propContentLength == null && textContentLength == null;
int actualPropLength = 0;
if (propContentLength != null || isOldVersion) {
String delta = (String) headers.get(SVNAdminHelper.DUMPFILE_PROP_DELTA);
boolean isDelta = delta != null && "true".equals(delta);
if (foundNode && !isDelta) {
handler.removeNodeProperties();
}
int length = 0;
try {
length = Integer.parseInt(propContentLength != null ? propContentLength : contentLength);
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header: can't parse property block length header");
SVNErrorManager.error(err, nfe);
}
actualPropLength += handler.parsePropertyBlock(dumpStream, length, foundNode);
}
if (textContentLength != null) {
String delta = (String) headers.get(SVNAdminHelper.DUMPFILE_TEXT_DELTA);
boolean isDelta = delta != null && "true".equals(delta);
int length = 0;
try {
length = Integer.parseInt(textContentLength);
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header: can't parse text block length header");
SVNErrorManager.error(err, nfe);
}
handler.parseTextBlock(dumpStream, length, isDelta);
} else if (isOldVersion) {
int length = 0;
try {
length = Integer.parseInt(contentLength);
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header: can't parse content length header");
SVNErrorManager.error(err, nfe);
}
length -= actualPropLength;
if (length > 0 || SVNNodeKind.parseKind((String)headers.get(SVNAdminHelper.DUMPFILE_NODE_KIND)) == SVNNodeKind.FILE) {
handler.parseTextBlock(dumpStream, length, false);
}
}
if (contentLength != null && !isOldVersion) {
int remaining = 0;
try {
remaining = Integer.parseInt(contentLength);
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header: can't parse content length header");
SVNErrorManager.error(err, nfe);
}
int propertyContentLength = 0;
if (propContentLength != null) {
try {
propertyContentLength = Integer.parseInt(propContentLength);
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header: can't parse property block length header");
SVNErrorManager.error(err, nfe);
}
}
remaining -= propertyContentLength;
int txtContentLength = 0;
if (textContentLength != null) {
try {
txtContentLength = Integer.parseInt(textContentLength);
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpfile header: can't parse text block length header");
SVNErrorManager.error(err, nfe);
}
}
remaining -= txtContentLength;
if (remaining < 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Sum of subblock sizes larger than total block content length");
SVNErrorManager.error(err);
}
byte buf[] = new byte[SVNAdminHelper.STREAM_CHUNK_SIZE];
while (remaining > 0) {
int numToRead = remaining >= SVNAdminHelper.STREAM_CHUNK_SIZE ? SVNAdminHelper.STREAM_CHUNK_SIZE : remaining;
int numRead = dumpStream.read(buf, 0, numToRead);
remaining -= numRead;
if (numRead != numToRead) {
SVNAdminHelper.generateIncompleteDataError();
}
}
}
if (foundNode) {
handler.closeNode();
foundNode = false;
}
}
handler.closeRevision();
} catch (IOException ioe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
SVNErrorManager.error(err, ioe);
}
}
private void dump(FSFS fsfs, OutputStream dumpStream, long start, long end, boolean isIncremental, boolean useDeltas) throws SVNException, IOException {
boolean isDumping = dumpStream != null;
long youngestRevision = fsfs.getYoungestRevision();
if (!SVNRevision.isValidRevisionNumber(start)) {
start = 0;
}
if (!SVNRevision.isValidRevisionNumber(end)) {
end = youngestRevision;
}
if (dumpStream == null) {
dumpStream = SVNFileUtil.DUMMY_OUT;
}
if (start > end) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS, "Start revision {0,number,integer} is greater than end revision {1,number,integer}", new Object[]{new Long(start), new Long(end)});
SVNErrorManager.error(err);
}
if (end > youngestRevision) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS, "End revision {0,number,integer} is invalid (youngest revision is {1,number,integer})", new Object[]{new Long(end), new Long(youngestRevision)});
SVNErrorManager.error(err);
}
if (start == 0 && isIncremental) {
isIncremental = false;
}
String uuid = fsfs.getUUID();
int version = SVNAdminHelper.DUMPFILE_FORMAT_VERSION;
if (!useDeltas) {
//for compatibility with SVN 1.0.x
version--;
}
writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_MAGIC_HEADER + ": " + version + "\n\n");
writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_UUID + ": " + uuid + "\n\n");
for (long i = start; i <= end; i++) {
long fromRev, toRev;
checkCancelled();
if (i == start && !isIncremental) {
if (i == 0) {
writeRevisionRecord(dumpStream, fsfs, 0);
toRev = 0;
String message = (isDumping ? "* Dumped" : "* Verified") + " revision " + toRev + ".";
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(toRev, SVNAdminEventAction.REVISION_DUMPED, message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
continue;
}
fromRev = 0;
toRev = i;
} else {
fromRev = i - 1;
toRev = i;
}
writeRevisionRecord(dumpStream, fsfs, toRev);
boolean useDeltasForRevision = useDeltas && (isIncremental || i != start);
FSRevisionRoot toRoot = fsfs.createRevisionRoot(toRev);
ISVNEditor dumpEditor = new SVNDumpEditor(fsfs, toRoot, toRev, start, "/", dumpStream, useDeltasForRevision);
if (i == start && !isIncremental) {
FSRevisionRoot fromRoot = fsfs.createRevisionRoot(fromRev);
SVNAdminHelper.deltifyDir(fsfs, fromRoot, "/", "", toRoot, "/", dumpEditor);
} else {
FSRepositoryUtil.replay(fsfs, toRoot, "", -1, false, dumpEditor);
}
String message = (isDumping ? "* Dumped" : "* Verified") + " revision " + toRev + ".";
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(toRev, SVNAdminEventAction.REVISION_DUMPED, message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
}
private void writeRevisionRecord(OutputStream dumpStream, FSFS fsfs, long revision) throws SVNException, IOException {
Map revProps = fsfs.getRevisionProperties(revision);
String revisionDate = (String) revProps.get(SVNRevisionProperty.DATE);
if (revisionDate != null) {
SVNDate date = SVNDate.parseDatestamp(revisionDate);
revProps.put(SVNRevisionProperty.DATE, date.format());
}
ByteArrayOutputStream encodedProps = new ByteArrayOutputStream();
SVNAdminHelper.writeProperties(revProps, null, encodedProps);
writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_REVISION_NUMBER + ": " + revision + "\n");
String propContents = new String(encodedProps.toByteArray(), "UTF-8");
writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_PROP_CONTENT_LENGTH + ": " + propContents.length() + "\n");
writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_CONTENT_LENGTH + ": " + propContents.length() + "\n\n");
writeDumpData(dumpStream, propContents);
dumpStream.write('\n');
}
private void writeDumpData(OutputStream out, String data) throws IOException {
out.write(data.getBytes("UTF-8"));
}
private ISVNLoadHandler getLoadHandler(File repositoryRoot, boolean usePreCommitHook, boolean usePostCommitHook, SVNUUIDAction uuidAction, String parentDir, CharsetDecoder decoder) throws SVNException {
if (myLoadHandler == null) {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot);
DefaultLoadHandler handler = new DefaultLoadHandler(usePreCommitHook, usePostCommitHook, uuidAction, parentDir, myEventHandler, decoder);
handler.setFSFS(fsfs);
myLoadHandler = handler;
} else {
myLoadHandler.setUsePreCommitHook(usePreCommitHook);
myLoadHandler.setUsePostCommitHook(usePostCommitHook);
myLoadHandler.setUUIDAction(uuidAction);
myLoadHandler.setParentDir(parentDir);
}
return myLoadHandler;
}
private Map readHeaderBlock(InputStream dumpStream, String firstHeader, CharsetDecoder decoder) throws SVNException, IOException {
Map headers = new HashMap();
StringBuffer buffer = new StringBuffer();
while (true) {
String header = null;
buffer.setLength(0);
if (firstHeader != null) {
header = firstHeader;
firstHeader = null;
} else {
header = SVNFileUtil.readLineFromStream(dumpStream, buffer, decoder);
if (header == null && buffer.length() > 0) {
SVNAdminHelper.generateIncompleteDataError();
} else if (buffer.length() == 0) {
break;
}
}
int colonInd = header.indexOf(':');
if (colonInd == -1) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Dump stream contains a malformed header (with no '':'') at ''{0}''", header.length() > 20 ? header.substring(0, 19) : header);
SVNErrorManager.error(err);
}
String name = header.substring(0, colonInd);
if (colonInd + 2 > header.length()) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Dump stream contains a malformed header (with no value) at ''{0}''", header.length() > 20 ? header.substring(0, 19) : header);
SVNErrorManager.error(err);
}
String value = header.substring(colonInd + 2);
headers.put(name, value);
}
return headers;
}
private void copyRevisionProperties(SVNRepository fromRepository, SVNRepository toRepository, long revision, boolean sync) throws SVNException {
Map existingRevProps = null;
if (sync) {
existingRevProps = toRepository.getRevisionProperties(revision, null);
}
boolean sawSyncProperties = false;
Map revProps = fromRepository.getRevisionProperties(revision, null);
for (Iterator propNames = revProps.keySet().iterator(); propNames.hasNext();) {
String propName = (String) propNames.next();
String propValue = (String) revProps.get(propName);
if (propName.startsWith("sync-")) {
sawSyncProperties = true;
} else {
toRepository.setRevisionPropertyValue(revision, propName, propValue);
}
if (sync) {
existingRevProps.remove(propName);
}
}
if (sync) {
for (Iterator propNames = existingRevProps.keySet().iterator(); propNames.hasNext();) {
String propName = (String) propNames.next();
toRepository.setRevisionPropertyValue(revision, propName, null);
}
}
if (sawSyncProperties) {
SVNDebugLog.getDefaultLog().info("Copied properties for revision " + revision + " (sync-* properties skipped).\n");
} else {
SVNDebugLog.getDefaultLog().info("Copied properties for revision " + revision + ".\n");
}
}
private SessionInfo openSourceRepository(SVNRepository targetRepos) throws SVNException {
String fromURL = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL);
String fromUUID = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID);
String lastMergedRev = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION);
if (fromURL == null || fromUUID == null || lastMergedRev == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Destination repository has not been initialized");
SVNErrorManager.error(err);
}
SVNURL srcURL = SVNURL.parseURIDecoded(fromURL);
// TOOD close session.
SVNRepository srcRepos = createRepository(srcURL, false);
checkIfRepositoryIsAtRoot(srcRepos, srcURL);
String reposUUID = srcRepos.getRepositoryUUID(true);
if (!fromUUID.equals(reposUUID)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "UUID of destination repository ({0}) does not match expected UUID ({1})", new String[] {
reposUUID, fromUUID
});
SVNErrorManager.error(err);
}
return new SessionInfo(srcRepos, Long.parseLong(lastMergedRev));
}
private void checkIfRepositoryIsAtRoot(SVNRepository repos, SVNURL url) throws SVNException {
SVNURL reposRoot = repos.getRepositoryRoot(true);
if (!reposRoot.equals(url)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Session is rooted at ''{0}'' but the repos root is ''{1}''", new SVNURL[] {
url, reposRoot
});
SVNErrorManager.error(err);
}
}
private void lock(SVNRepository repos) throws SVNException {
String hostName = null;
try {
hostName = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Can't get local hostname");
SVNErrorManager.error(err, e);
}
if (hostName.length() > 256) {
hostName = hostName.substring(0, 256);
}
String lockToken = hostName + ":" + SVNUUIDGenerator.formatUUID(SVNUUIDGenerator.generateUUID());
int i = 0;
for (i = 0; i < 10; i++) {
String reposLockToken = repos.getRevisionPropertyValue(0, SVNRevisionProperty.LOCK);
if (reposLockToken != null) {
if (reposLockToken.equals(lockToken)) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//
}
} else {
repos.setRevisionPropertyValue(0, SVNRevisionProperty.LOCK, lockToken);
}
}
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Couldn''t get lock on destination repos after {0,number,integer} attempts\n", new Integer(i));
SVNErrorManager.error(err);
}
private void unlock(SVNRepository repos) throws SVNException {
repos.setRevisionPropertyValue(0, SVNRevisionProperty.LOCK, null);
}
private class SessionInfo {
SVNRepository myRepository;
long myLastMergedRevision;
public SessionInfo(SVNRepository repos, long lastMergedRev) {
myRepository = repos;
myLastMergedRevision = lastMergedRev;
}
}
}
|