/*
* ====================================================================
* Copyright (c) 2004-2012 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.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNDepth;
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.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
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.FSFS;
import org.tmatesoft.svn.core.internal.io.fs.FSHotCopier;
import org.tmatesoft.svn.core.internal.io.fs.FSPacker;
import org.tmatesoft.svn.core.internal.io.fs.FSRecoverer;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryUtil;
import org.tmatesoft.svn.core.internal.io.fs.FSRevisionRoot;
import org.tmatesoft.svn.core.internal.io.fs.FSRoot;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNUUIDGenerator;
import org.tmatesoft.svn.core.internal.wc.DefaultDumpFilterHandler;
import org.tmatesoft.svn.core.internal.wc.DefaultLoadHandler;
import org.tmatesoft.svn.core.internal.wc.ISVNLoadHandler;
import org.tmatesoft.svn.core.internal.wc.SVNAdminDeltifier;
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.SVNDumpStreamParser;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNPropertiesManager;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.ISVNLockHandler;
import org.tmatesoft.svn.core.io.SVNCapability;
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.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnOperationFactory;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;
/**
* 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>doInfo()</td><td>'svnsync info'</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>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doPack()</td><td>'svnadmin pack'</td>
* </tr>
* </table>
*
* @version 1.3
* @author TMate Software Ltd.
* @since 1.2
*/
public class SVNAdminClient extends SVNAdminBasicClient {
private ISVNLogEntryHandler mySyncHandler;
private DefaultDumpFilterHandler myDumpFilterHandler;
private ISVNAdminEventHandler myEventHandler;
private FSHotCopier myHotCopier;
private SVNDumpStreamParser myDumpStreamParser;
private SVNDumpEditor myDumpEditor;
private static final int LOCK_RETRY_COUNT = 10;
/**
* 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);
}
public SVNAdminClient(SvnOperationFactory of) {
super(of);
}
/**
* 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. But it does not mean that you can have two handler set, only
* one handler can be used at a time.
*
* @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 doCreateRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, false);
}
/**
* 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, <code>pre15Compatible</code> - with pre-1.5 servers and
* <code>pre16Compatible</code> - with pre-1.6 servers.
*
* <p>
* There must be only one option (either <code>pre14Compatible</code> or <code>pre15Compatible</code> or <code>pre16Compatible</code>)
* set to <span class="javakeyword">true</span> at a time.
*
* @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
* @param pre15Compatible <span class="javakeyword">true</span> to
* create a repository with pre-1.5 format
* @param pre16Compatible <span class="javakeyword">true</span> to
* create a repository with pre-1.6 format
* @return a local URL (file:///) of a newly created repository
* @throws SVNException
* @since 1.3, SVN 1.5.0
*/
public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force,
boolean pre14Compatible, boolean pre15Compatible, boolean pre16Compatible) throws SVNException {
return SVNRepositoryFactory.createLocalRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, pre15Compatible, pre16Compatible);
}
public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force,
boolean pre14Compatible, boolean pre15Compatible, boolean pre16Compatible, boolean pre17Compatible, boolean with17Compatible) throws SVNException {
return SVNRepositoryFactory.createLocalRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, pre15Compatible, pre16Compatible, pre17Compatible, with17Compatible);
}
/**
* 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, <code>pre15Compatible</code> - with pre-1.5 servers.
*
* <p>
* There must be only one option (either <code>pre14Compatible</code> or <code>pre15Compatible</code>)
* set to <span class="javakeyword">true</span> at a time.
*
* @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
* @param pre15Compatible <span class="javakeyword">true</span> to
* create a repository with pre-1.5 format
* @return a local URL (file:///) of a newly created repository
* @throws SVNException
* @since 1.2, SVN 1.5.0
*/
public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force,
boolean pre14Compatible, boolean pre15Compatible) throws SVNException {
return doCreateRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, pre15Compatible, false);
}
/**
* Copies revision properties from the source repository starting at <code>startRevision</code> and up to
* <code>endRevision</code> to corresponding revisions of the destination repository represented by
* <code>toURL</code>.
*
* <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.
*
* <p/>
* If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
* receive an {@link SVNAdminEvent} with the {@link SVNAdminEventAction#REVISION_PROPERTIES_COPIED} action
* when the properties get copied.
*
* @param toURL a url to the destination repository which must be synchronized
* with another repository
* @param startRevision start revision
* @param endRevision end revision
* @throws SVNException in the following cases:
* <ul>
* <li/>exception with {@link SVNErrorCode#IO_ERROR} error code - if any of revisions
* between <code>startRevision</code> and <code>endRevision</code> inclusively was not
* synchronized yet
* </ul>
* @since 1.2.0, new in Subversion 1.5.0
*/
public void doCopyRevisionProperties(SVNURL toURL, long startRevision, long endRevision) throws SVNException {
SVNRepository toRepos = null;
SessionInfo info = null;
SVNException error = null;
SVNException error2 = null;
try {
toRepos = createRepository(toURL, null, true);
checkIfRepositoryIsAtRoot(toRepos, toURL);
lock(toRepos);
info = openSourceRepository(toRepos);
if (!SVNRevision.isValidRevisionNumber(startRevision)) {
startRevision = info.myLastMergedRevision;
}
if (!SVNRevision.isValidRevisionNumber(endRevision)) {
endRevision = info.myLastMergedRevision;
}
if (startRevision > info.myLastMergedRevision) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
"Cannot copy revprops for a revision ({0}) that has not been synchronized yet",
String.valueOf(startRevision));
SVNErrorManager.error(err, SVNLogType.FSFS);
}
if (endRevision > info.myLastMergedRevision) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
"Cannot copy revprops for a revision ({0}) that has not been synchronized yet",
String.valueOf(endRevision));
SVNErrorManager.error(err, SVNLogType.FSFS);
}
int normalizedRevPropsCount = 0;
long step = startRevision > endRevision ? -1 : 1;
for (long i = startRevision; i != endRevision + step; i += step) {
checkCancelled();
SVNProperties normalizedProps = copyRevisionProperties(info.myRepository, toRepos, i, false);
normalizedRevPropsCount += normalizedProps.size();
}
handleNormalizedProperties(normalizedRevPropsCount, 0);
} catch (SVNException svne) {
error = svne;
} finally {
try {
unlock(toRepos);
if (toRepos != null) {
toRepos.closeSession();
}
if (info != null && info.myRepository != null) {
info.myRepository.closeSession();
}
} 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 in the following cases:
* <ul>
* <li/>exception with {@link SVNErrorCode#IO_ERROR} error code - if
* either the target repository's last revision is different from <code>0</code>, or
* no {@link SVNRevisionProperty#FROM_URL} property value found in the target
* repository
* <li/>exception with {@link SVNErrorCode#RA_PARTIAL_REPLAY_NOT_SUPPORTED} error
* code - if the source repository does not support partial replay
* </ul>
* @since 1.1, new in Subversion 1.4
*/
public void doInitialize(SVNURL fromURL, SVNURL toURL) throws SVNException {
SVNRepository toRepos = null;
SVNRepository fromRepos = null;
SVNException error = null;
SVNException error2 = null;
try {
toRepos = createRepository(toURL, null, true);
checkIfRepositoryIsAtRoot(toRepos, toURL);
lock(toRepos);
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, SVNLogType.FSFS);
}
SVNPropertyValue 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, SVNLogType.FSFS);
}
fromRepos = createRepository(fromURL, null, false);
SVNURL rootURL = fromRepos.getRepositoryRoot(true);
if (SVNPathUtil.getPathAsChild(rootURL.toString(), fromURL.toString()) != null) {
boolean supportsPartialReplay = false;
try {
supportsPartialReplay = fromRepos.hasCapability(SVNCapability.PARTIAL_REPLAY);
} catch (SVNException svne) {
if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.UNSUPPORTED_FEATURE) {
throw svne;
}
}
if (!supportsPartialReplay) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_PARTIAL_REPLAY_NOT_SUPPORTED);
SVNErrorManager.error(err, SVNLogType.FSFS);
}
}
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL,
SVNPropertyValue.create(fromURL.toString()));
String uuid = fromRepos.getRepositoryUUID(true);
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID, SVNPropertyValue.create(uuid));
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION,
SVNPropertyValue.create("0"));
SVNProperties normalizedProps = copyRevisionProperties(fromRepos, toRepos, 0, false);
handleNormalizedProperties(normalizedProps.size(), 0);
} catch (SVNException svne) {
error = svne;
} finally {
try {
unlock(toRepos);
if (toRepos != null) {
toRepos.closeSession();
}
if (fromRepos != null) {
fromRepos.closeSession();
}
} catch (SVNException svne) {
error2 = svne;
}
}
if (error != null) {
throw error;
} else if (error2 != null) {
throw error2;
}
}
/**
* Returns information about the synchronization repository located at <code>toURL</code>.
*
* @param toURL destination repository url
* @return synchronization information
* @throws SVNException
* @since 1.3, SVN 1.6
*/
public SVNSyncInfo doInfo(SVNURL toURL) throws SVNException {
SVNRepository toRepos = null;
try {
toRepos = createRepository(toURL, null, true);
checkIfRepositoryIsAtRoot(toRepos, toURL);
SVNPropertyValue fromURL = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL);
if (fromURL == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Repository ''{0}'' is not initialized for synchronization",
toURL);
SVNErrorManager.error(err, SVNLogType.FSFS);
}
SVNPropertyValue fromUUID = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID);
SVNPropertyValue lastMergedRevProp = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION);
long lastMergedRev = lastMergedRevProp != null ? Long.parseLong(lastMergedRevProp.getString()) : SVNRepository.INVALID_REVISION;
return new SVNSyncInfo(fromURL.getString(), fromUUID != null ? fromUUID.getString() : null, lastMergedRev);
} finally {
if (toRepos != null) {
toRepos.closeSession();
}
}
}
/**
* Compacts a repository into a more efficient storage model.
*
* <p/>
* Compacting does not occur if there are no full shards. Also compacting does not work
* on pre-1.6 repositories.
*
* @param repositoryRoot root of the repository to pack
* @throws SVNException
* @since 1.3, SVN 1.6
*/
public void doPack(File repositoryRoot) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
try {
FSPacker packer = new FSPacker(myEventHandler);
packer.pack(fsfs);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* 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 = null;
SVNRepository toRepos = null;
try {
fromRepos = createRepository(fromURL, null, true);
toRepos = createRepository(toURL, null, false);
replicator.replicateRepository(fromRepos, toRepos, 1, -1);
} finally {
if (fromRepos != null) {
fromRepos.closeSession();
}
if (toRepos != null) {
toRepos.closeSession();
}
}
}
/**
* 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 = null;
SVNRepository fromRepos = null;
SVNException error = null;
SVNException error2 = null;
try {
toRepos = createRepository(toURL, null, true);
checkIfRepositoryIsAtRoot(toRepos, toURL);
lock(toRepos);
SessionInfo info = openSourceRepository(toRepos);
fromRepos = info.myRepository;
long lastMergedRevision = info.myLastMergedRevision;
SVNPropertyValue currentlyCopying = toRepos.getRevisionPropertyValue(0,
SVNRevisionProperty.CURRENTLY_COPYING);
long toLatestRevision = toRepos.getLatestRevision();
int normalizedRevPropsCount = 0;
if (currentlyCopying != null) {
long copyingRev = -1;
try {
copyingRev = Long.parseLong(currentlyCopying.getString());
} catch (NumberFormatException nfe) {
SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.IO_ERROR, nfe), SVNLogType.WC);
}
if (copyingRev < lastMergedRevision || copyingRev > lastMergedRevision + 1 ||
(toLatestRevision != lastMergedRevision && toLatestRevision != copyingRev)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
"Revision being currently copied ({0}), last merged revision ({1}), and destination HEAD ({2}) are inconsistent; have you committed to the destination without using svnsync?",
new Object[] { String.valueOf(copyingRev), String.valueOf(lastMergedRevision),
String.valueOf(toLatestRevision) });
SVNErrorManager.error(err, SVNLogType.FSFS);
} else if (copyingRev == toLatestRevision) {
if (copyingRev > lastMergedRevision) {
SVNProperties normalizedProps = copyRevisionProperties(fromRepos, toRepos, toLatestRevision, true);
normalizedRevPropsCount += normalizedProps.size();
lastMergedRevision = copyingRev;
}
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION, SVNPropertyValue.create(SVNProperty.toString(lastMergedRevision)));
toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.CURRENTLY_COPYING, null);
}
} else {
if (toLatestRevision != lastMergedRevision) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
"Destination HEAD ({0}) is not the last merged revision ({1}); have you committed to the destination without using svnsync?",
new Object[] { String.valueOf(toLatestRevision), String.valueOf(lastMergedRevision) });
SVNErrorManager.error(err, SVNLogType.FSFS);
}
}
long fromLatestRevision = fromRepos.getLatestRevision();
if (fromLatestRevision < lastMergedRevision) {
return;
}
boolean hasCommitRevPropCapability = toRepos.hasCapability(SVNCapability.COMMIT_REVPROPS);
checkCancelled();
long startRevision = lastMergedRevision + 1;
long endRevision = fromLatestRevision;
SVNReplayHandler replayHandler = new SVNReplayHandler(toRepos, hasCommitRevPropCapability,
mySyncHandler, getDebugLog(), this, this);
fromRepos.replayRange(startRevision, endRevision, 0, true, replayHandler);
handleNormalizedProperties(normalizedRevPropsCount + replayHandler.getNormalizedRevPropsCount(),
replayHandler.getNormalizedNodePropsCount());
} catch (SVNException svne) {
error = svne;
} finally {
try {
unlock(toRepos);
if (toRepos != null) {
toRepos.closeSession();
}
if (fromRepos != null) {
fromRepos.closeSession();
}
} catch (SVNException svne) {
error2 = svne;
}
}
if (error != null) {
throw error;
} else if (error2 != null) {
throw error2;
}
}
/**
* Walks through the repository tree found under <code>repositoryRoot</code> and reports all found locks
* via calls to the caller's {@link ISVNAdminEventHandler} handler implementation.
*
* <p/>
* On each locked path found this method dispatches an {@link SVNAdminEventAction#LOCK_LISTED}
* {@link SVNAdminEvent event} to the caller's handler providing the
* {@link SVNAdminEvent#getLock() lock information}.
*
* @param repositoryRoot repository root location
* @throws SVNException
* @since 1.2.0, SVN 1.5.0
*/
public void doListLocks(File repositoryRoot) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
try {
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);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* Removes locks from the specified <code>paths</code> in the repository found under
* <code>repositoryRoot</code>.
*
* <p/>
* If any path of the <code>paths</code> is not locked, then an {@link SVNAdminEvent event} with the
* {@link SVNAdminEventAction#NOT_LOCKED} action is dispatched to the caller's {@link ISVNAdminEventHandler}
* handler.
*
* <p/>
* If, on the contrary, a path is locked, it is unlocked and an event with the
* {@link SVNAdminEventAction#UNLOCKED} action is dispatched to the caller's handler providing the
* {@link SVNAdminEvent#getLock() lock information}.
*
* <p/>
* If some error occurs while unlocking, an event with the {@link SVNAdminEventAction#UNLOCK_FAILED} action
* is dispatched to the caller's handler providing the {@link SVNAdminEvent#getError() error description}.
*
* @param repositoryRoot repository root location
* @param paths paths to unlock
* @throws SVNException
* @since 1.2.0, SVN 1.5.0
*/
public void doRemoveLocks(File repositoryRoot, String[] paths) throws SVNException {
if (paths == null) {
return;
}
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
try {
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, null, 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);
}
}
}
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* 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, true);
try {
Map txns = fsfs.listTransactions();
for(Iterator names = txns.keySet().iterator(); names.hasNext();) {
String txnName = (String) names.next();
File txnDir = (File) txns.get(txnName);
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(txnName, txnDir, SVNAdminEventAction.TRANSACTION_LISTED);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* 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, true);
try {
for (int i = 0; i < transactions.length; i++) {
String txnName = transactions[i];
fsfs.openTxn(txnName);
fsfs.purgeTxn(txnName);
SVNDebugLog.getDefaultLog().logFine(SVNLogType.FSFS, "Transaction '" + txnName + "' removed.\n");
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(txnName, fsfs.getTransactionDir(txnName), SVNAdminEventAction.TRANSACTION_REMOVED);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* 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.
*
* <p/>
* This is identical to <code>doVerify(repositoryRoot, SVNRevision.create(0), SVNRevision.HEAD)</code>.
*
* @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);
}
/**
* Verifies repository contents found under <code>repositoryRoot</code> starting at <code>startRevision</code>
* and up to <code>endRevision</code>. This method uses the dump implementation
* (non incremental) passing a dummy output stream to it. This allows to check the integrity of the
* repository data.
*
* <p/>
* If <code>startRevision</code> is {@link SVNRevision#isValid() invalid}, it defaults to <code>0</code>.
* If <code>endRevision</code> is {@link SVNRevision#isValid() invalid}, it defaults to the HEAD revision.
*
* @param repositoryRoot a repository root directory path
* @param startRevision revision to start verification at
* @param endRevision revision to stop verification at
* @throws SVNException verification failed - a repository may be corrupted
* @since 1.2.0, SVN 1.5.0
*/
public void doVerify(File repositoryRoot, SVNRevision startRevision, SVNRevision endRevision) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
try {
long youngestRevision = fsfs.getYoungestRevision();
long lowerRev = SVNAdminHelper.getRevisionNumber(startRevision, youngestRevision, fsfs);
long upperRev = SVNAdminHelper.getRevisionNumber(endRevision, youngestRevision, fsfs);
if (!SVNRevision.isValidRevisionNumber(upperRev)) {
upperRev = lowerRev;
}
verify(fsfs, lowerRev, upperRev);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* 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, true);
try {
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, SVNLogType.FSFS);
}
dump(fsfs, dumpStream, lowerR, upperR, isIncremental, useDeltas);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* 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();
FSFS fsfs = null;
try {
fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
ISVNLoadHandler handler = createLoadHandler(fsfs, usePreCommitHook, usePostCommitHook,
uuidAction, parentDir);
SVNDumpStreamParser parser = getDumpStreamParser();
parser.parseDumpStream(dumpStream, handler, decoder);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* Recovers the repository found under <code>repositoryRoot</code>.
* This method can recover only FSFS type repositories and is identical to the <code>'svnadmin recover'</code>
* command.
*
* <p/>
* If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
* receive an {@link SVNAdminEvent} with the {@link SVNAdminEventAction#RECOVERY_STARTED} action before
* the recovery starts.
*
* @param repositoryRoot repository root location
* @throws SVNException
* @since 1.2.0, SVN 1.5.0
*/
public void doRecover(File repositoryRoot) throws SVNException {
FSFS fsfs = null;
try {
fsfs = SVNAdminHelper.openRepositoryForRecovery(repositoryRoot);
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.RECOVERY_STARTED);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
FSRecoverer recoverer = new FSRecoverer(fsfs, this);
recoverer.runRecovery();
} finally {
if (fsfs != null) {
fsfs.close();
}
}
}
/**
* Upgrades the repository located at <code>repositoryRoot</code> to the latest supported
* schema version. This method is identical to the <code>'svnadmin upgrade'</code> command.
*
* <p/>
* This functionality is provided as a convenience for repository administrators who wish to make use of
* new Subversion functionality without having to undertake a potentially costly full repository dump
* and load operation. As such, the upgrade performs only the minimum amount of work needed to accomplish
* this while still maintaining the integrity of the repository. It does not guarantee the most optimized
* repository state as a dump and subsequent load would.
*
* <p/>
* If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
* receive an {@link SVNAdminEvent} with the {@link SVNAdminEventAction#UPGRADE} action before
* the upgrade starts.
*
* @param repositoryRoot repository root location
* @throws SVNException
* @since 1.2.0, SVN 1.5.0
*/
public void doUpgrade(File repositoryRoot)throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
try {
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.UPGRADE);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
File reposFormatFile = fsfs.getRepositoryFormatFile();
int format = fsfs.getReposFormat();
SVNFileUtil.writeVersionFile(reposFormatFile, format);
fsfs.upgrade();
SVNFileUtil.writeVersionFile(reposFormatFile, FSFS.REPOSITORY_FORMAT);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* Resets the repository UUID with the provided <code>uuid</code> for the repository located at
* <code>repositoryRoot</code>. This method is identical to the <code>'svnadmin setuuid'</code> command.
*
* <p/>
* If no <code>uuid</code> is specified, then <code>SVNKit</code> will generate a new one and will use it to
* reset the original UUID.
*
* @param repositoryRoot repository root location
* @param uuid new UUID to set
* @throws SVNException exception with {@link SVNErrorCode#BAD_UUID} error code - if the <code>uuid</code>
* is malformed
* @since 1.2.0, SVN 1.5.0
*/
public void doSetUUID(File repositoryRoot, String uuid) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
try {
if (uuid == null) {
uuid = SVNUUIDGenerator.generateUUIDString();
} else {
String[] components = uuid.split("-");
if (components.length != 5) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_UUID, "Malformed UUID ''{0}''",
uuid);
SVNErrorManager.error(err, SVNLogType.FSFS);
}
}
fsfs.setUUID(uuid);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* Makes a hot copy of a repository located at <code>srcRepositoryRoot</code> to one located at
* <code>newRepositoryRoot</code>. This method is identical to the <code>'svnadmin hotcopy'</code> command.
*
* @param srcRepositoryRoot repository to copy data from
* @param newRepositoryRoot repository to copy data to
* @throws SVNException
* @since 1.2.0, SVN 1.5.0
*/
public void doHotCopy(File srcRepositoryRoot, File newRepositoryRoot) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(srcRepositoryRoot, false);
try {
FSHotCopier copier = getHotCopier();
copier.runHotCopy(fsfs, newRepositoryRoot);
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* Returns the HEAD revision of the repository located at <code>repositoryRoot</code>.
*
* <p/>
* Identical to the <code>'svnlook youngest'</code> svn command.
*
* @param repositoryRoot repository root location
* @return the last revision
* @throws SVNException
* @since 1.2.0
*/
public long getYoungestRevision(File repositoryRoot) throws SVNException {
FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
try {
return fsfs.getYoungestRevision();
} finally {
SVNAdminHelper.closeRepository(fsfs);
}
}
/**
* Filters out nodes with or without the given <code>prefixes</code> from <code>dumpStream</code>
* to <code>resultDumpStream</code>. This method is similar to the functionality provided by the
* <code>'svndumpfilter'</code> utility.
*
* <p/>
* If <code>exclude</code> is <span class="javakeyword">true</span> then filters out nodes with
* <code>prefixes</code>, otherwise nodes without <code>prefixes</code>.
*
* <p/>
* If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
* be called with different actions:
* <ul>
* <li/>{@link SVNAdminEventAction#DUMP_FILTER_TOTAL_REVISIONS_DROPPED} - use {@link SVNAdminEvent#getDroppedRevisionsCount()}
* to retrieve the total number of dropped revisions.
* <li/>{@link SVNAdminEventAction#DUMP_FILTER_DROPPED_RENUMBERED_REVISION} - is sent only when
* <code>renumberRevisions</code> is <span class="javakeyword">true</span> and informs that an original
* revision (which is provided as {@link SVNAdminEvent#getRevision()}) was dropped.
* <li/>{@link SVNAdminEventAction#DUMP_FILTER_RENUMBERED_REVISION} - is sent only when
* <code>renumberRevisions</code> is <span class="javakeyword">true</span> and informs that the original
* revision (provided as {@link SVNAdminEvent#getOriginalRevision()}) was renumbered to {@link SVNAdminEvent#getRevision()}.
* <li/>{@link SVNAdminEventAction#DUMP_FILTER_DROPPED_NODE} - says that {@link SVNAdminEvent#getPath()} was
* dropped.
* <li/>{@link SVNAdminEventAction#DUMP_FILTER_TOTAL_NODES_DROPPED} - use {@link SVNAdminEvent#getDroppedNodesCount()}
* to retrieve the total number of dropped nodes.
* <li/>{@link SVNAdminEventAction#DUMP_FILTER_REVISION_COMMITTED} - is sent to inform that the original
* revision {@link SVNAdminEvent#getOriginalRevision()} resulted in {@link SVNAdminEvent#getRevision()}
* in the output.
* <li/>{@link SVNAdminEventAction#DUMP_FILTER_REVISION_SKIPPED} - is sent to inform that the original
* revision {@link SVNAdminEvent#getRevision()} is dropped (skipped).
* </ul>
*
* @param dumpStream the input repository dump stream
* @param resultDumpStream the resultant (filtered) dump stream
* @param exclude whether to exclude or include paths with the specified
* <code>prefixes</code>
* @param renumberRevisions if <span class="javakeyword">true</span>, renumbers revisions left
* after filtering
* @param dropEmptyRevisions if <span class="javakeyword">true</span>, then removes revisions
* emptied by filtering
* @param preserveRevisionProperties if <span class="javakeyword">true</span>, then does not filter
* revision properties
* @param prefixes prefixes of the path to filter
* @param skipMissingMergeSources if <span class="javakeyword">true</span>, then skips missig merge
* sources
* @throws SVNException
* @since 1.2.0, SVN 1.5.0
*/
public void doFilter(InputStream dumpStream, OutputStream resultDumpStream, boolean exclude,
boolean renumberRevisions, boolean dropEmptyRevisions, boolean preserveRevisionProperties,
Collection prefixes, boolean skipMissingMergeSources) throws SVNException {
CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
writeDumpData(resultDumpStream, SVNAdminHelper.DUMPFILE_MAGIC_HEADER + ": 2\n\n");
DefaultDumpFilterHandler handler = getDumpFilterHandler(resultDumpStream, exclude, renumberRevisions,
dropEmptyRevisions, preserveRevisionProperties, prefixes, skipMissingMergeSources);
SVNDumpStreamParser parser = getDumpStreamParser();
parser.parseDumpStream(dumpStream, handler, decoder);
if (myEventHandler != null) {
if (handler.getDroppedRevisionsCount() > 0) {
String message = MessageFormat.format("Dropped {0} revision(s).", new Object[] {
String.valueOf(handler.getDroppedRevisionsCount()) });
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.DUMP_FILTER_TOTAL_REVISIONS_DROPPED,
message);
event.setDroppedRevisionsCount(handler.getDroppedRevisionsCount());
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
if (renumberRevisions) {
Map renumberHistory = handler.getRenumberHistory();
Long[] reNumberedRevisions = (Long[]) renumberHistory.keySet().toArray(new Long[renumberHistory.size()]);
Arrays.sort(reNumberedRevisions);
for (int i = reNumberedRevisions.length; i > 0; i--) {
Long revision = reNumberedRevisions[i - 1];
DefaultDumpFilterHandler.RevisionItem revItem = (DefaultDumpFilterHandler.RevisionItem) renumberHistory.get(revision);
if (revItem.wasDropped()) {
String message = MessageFormat.format("{0} => (dropped)", new Object[] { revision.toString() });
SVNAdminEvent event = new SVNAdminEvent(revision.longValue(),
SVNAdminEventAction.DUMP_FILTER_DROPPED_RENUMBERED_REVISION, message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
} else {
String message = MessageFormat.format("{0} => {1}", new Object[] { revision.toString(),
String.valueOf(revItem.getRevision()) });
SVNAdminEvent event = new SVNAdminEvent(revItem.getRevision(), revision.longValue(),
SVNAdminEventAction.DUMP_FILTER_RENUMBERED_REVISION, message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
}
Map droppedNodes = handler.getDroppedNodes();
if (!droppedNodes.isEmpty()) {
String message = MessageFormat.format("Dropped {0} node(s)", new Object[] {
String.valueOf(droppedNodes.size()) });
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.DUMP_FILTER_TOTAL_NODES_DROPPED,
message);
event.setDroppedNodesCount(droppedNodes.size());
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
String[] paths = (String[]) droppedNodes.keySet().toArray(new String[droppedNodes.size()]);
Arrays.sort(paths, SVNPathUtil.PATH_COMPARATOR);
for (int i = 0; i < paths.length; i++) {
String path = paths[i];
message = "'" + path + "'";
event = new SVNAdminEvent(SVNAdminEventAction.DUMP_FILTER_DROPPED_NODE, path, message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
}
}
protected void handlePropertesCopied(boolean foundSyncProps, long revision) throws SVNException {
if (myEventHandler != null) {
String message = null;
if (foundSyncProps) {
message = MessageFormat.format("Copied properties for revision {0} ({1}* properties skipped).",
new Object[] { String.valueOf(revision), SVNProperty.SVN_SYNC_PREFIX });
} else {
message = MessageFormat.format("Copied properties for revision {0}.", new Object[] {
String.valueOf(revision) });
}
SVNAdminEvent event = new SVNAdminEvent(revision, SVNAdminEventAction.REVISION_PROPERTIES_COPIED,
message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
protected void handleNormalizedProperties(int normalizedRevPropsCount, int normalizedNodePropsCount) throws SVNException {
if (myEventHandler != null && (normalizedRevPropsCount > 0 || normalizedNodePropsCount > 0)) {
String message = MessageFormat.format("NOTE: Normalized {0}* properties to LF line endings ({1} rev-props, {2} node-props).",
new Object[] { SVNProperty.SVN_PREFIX, String.valueOf(normalizedRevPropsCount), String.valueOf(normalizedNodePropsCount) });
SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.NORMALIZED_PROPERTIES, message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
private FSHotCopier getHotCopier() {
if (myHotCopier == null) {
myHotCopier = new FSHotCopier();
}
return myHotCopier;
}
private void verify(FSFS fsfs, long startRev, long endRev) throws SVNException {
long youngestRev = fsfs.getYoungestRevision();
if (!SVNRevision.isValidRevisionNumber(startRev)) {
startRev = 0;
}
if (!SVNRevision.isValidRevisionNumber(endRev)) {
endRev = youngestRev;
}
if (startRev > endRev) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS, "Start revision {0} is greater than end revision {1}",
new Object[] { String.valueOf(startRev), String.valueOf(endRev) });
SVNErrorManager.error(err, SVNLogType.FSFS);
}
if (endRev > youngestRev) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS,
"End revision {0} is invalid (youngest revision is {1})", new Object[] { String.valueOf(endRev),
String.valueOf(youngestRev) });
SVNErrorManager.error(err, SVNLogType.FSFS);
}
for (long rev = startRev; rev <= endRev; rev++) {
FSRevisionRoot toRoot = fsfs.createRevisionRoot(rev);
ISVNEditor editor = getDumpEditor(fsfs, toRoot, rev, startRev, "/", SVNFileUtil.DUMMY_OUT, false, true);
editor = SVNCancellableEditor.newInstance(editor, getEventDispatcher(), getDebugLog());
FSRepositoryUtil.replay(fsfs, toRoot, "", SVNRepository.INVALID_REVISION, false, editor);
fsfs.getRevisionProperties(rev);
String message = "* Verified revision " + rev + ".";
if (myEventHandler != null) {
SVNAdminEvent event = new SVNAdminEvent(rev, SVNAdminEventAction.REVISION_DUMPED, message);
myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
}
}
}
private void dump(FSFS fsfs, OutputStream dumpStream, long start, long end, boolean isIncremental, boolean useDeltas) throws SVNException {
boolean isDumping = dumpStream != null && dumpStream != SVNFileUtil.DUMMY_OUT;
long youngestRevision = fsfs.getYoungestRevision();
SVNAdminDeltifier deltifier = new SVNAdminDeltifier(fsfs, SVNDepth.INFINITY,
false, false, false, null);
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} is greater than end revision {1}", new Object[] { String.valueOf(start),
String.valueOf(end) });
SVNErrorManager.error(err, SVNLogType.FSFS);
}
if (end > youngestRevision) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS,
"End revision {0} is invalid (youngest revision is {1})", new Object[] { String.valueOf(end),
String.valueOf(youngestRevision) });
SVNErrorManager.error(err, SVNLogType.FSFS);
}
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 = getDumpEditor(fsfs, toRoot, toRev, start, "/", dumpStream, useDeltasForRevision, false);
if (i == start && !isIncremental) {
FSRevisionRoot fromRoot = fsfs.createRevisionRoot(fromRev);
deltifier.setEditor(dumpEditor);
deltifier.deltifyDir(fromRoot, "/", "", toRoot, "/");
} 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 {
SVNProperties revProps = fsfs.getRevisionProperties(revision);
String revisionDate = revProps.getStringValue(SVNRevisionProperty.DATE);
if (revisionDate != null) {
SVNDate date = SVNDate.parseDate(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 = null;
try {
propContents = new String(encodedProps.toByteArray(), "UTF-8");
} catch (UnsupportedEncodingException uee) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, uee.getLocalizedMessage());
SVNErrorManager.error(err, uee, SVNLogType.FSFS);
}
writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_PROP_CONTENT_LENGTH + ": " + propContents.length() + "\n");
writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_CONTENT_LENGTH + ": " + propContents.length() + "\n\n");
writeDumpData(dumpStream, propContents);
writeDumpData(dumpStream, "\n");
}
private void writeDumpData(OutputStream out, String data) throws SVNException {
try {
out.write(data.getBytes("UTF-8"));
} catch (IOException ioe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
}
}
private DefaultLoadHandler createLoadHandler(FSFS fsfs, boolean usePreCommitHook,
boolean usePostCommitHook, SVNUUIDAction uuidAction, String parentDir) {
DefaultLoadHandler handler = new DefaultLoadHandler(usePreCommitHook, usePostCommitHook, uuidAction,
parentDir, myEventHandler);
handler.setFSFS(fsfs);
handler.setUsePreCommitHook(usePreCommitHook);
handler.setUsePostCommitHook(usePostCommitHook);
handler.setUUIDAction(uuidAction);
handler.setParentDir(parentDir);
return handler;
}
private DefaultDumpFilterHandler getDumpFilterHandler(OutputStream os, boolean exclude,
boolean renumberRevisions, boolean dropEmptyRevisions, boolean preserveRevisionProperties,
Collection prefixes, boolean skipMissingMergeSources) {
if (myDumpFilterHandler == null) {
myDumpFilterHandler = new DefaultDumpFilterHandler(os, myEventHandler, exclude, renumberRevisions,
dropEmptyRevisions, preserveRevisionProperties, prefixes, skipMissingMergeSources);
} else {
myDumpFilterHandler.reset(os, myEventHandler, exclude, renumberRevisions, dropEmptyRevisions,
preserveRevisionProperties, prefixes, skipMissingMergeSources);
}
return myDumpFilterHandler;
}
private SVNDumpStreamParser getDumpStreamParser() {
if (myDumpStreamParser == null) {
myDumpStreamParser = new SVNDumpStreamParser(this);
}
return myDumpStreamParser;
}
private SVNDumpEditor getDumpEditor(FSFS fsfs, FSRoot root, long toRevision, long oldestDumpedRevision, String rootPath, OutputStream dumpStream,
boolean useDeltas, boolean isVerify) {
if (myDumpEditor == null) {
myDumpEditor = new SVNDumpEditor(fsfs, root, toRevision, oldestDumpedRevision, rootPath, dumpStream, useDeltas, isVerify);
} else {
myDumpEditor.reset(fsfs, root, toRevision, oldestDumpedRevision, rootPath, dumpStream, useDeltas, isVerify);
}
return myDumpEditor;
}
private SVNProperties copyRevisionProperties(SVNRepository fromRepository, SVNRepository toRepository,
long revision, boolean sync) throws SVNException {
int filteredCount = 0;
SVNProperties existingRevProps = null;
if (sync) {
existingRevProps = toRepository.getRevisionProperties(revision, null);
}
SVNProperties revProps = fromRepository.getRevisionProperties(revision, null);
SVNProperties normalizedProps = normalizeRevisionProperties(revProps);
filteredCount += SVNAdminHelper.writeRevisionProperties(toRepository, revision, revProps);
if (sync) {
SVNAdminHelper.removePropertiesNotInSource(toRepository, revision, revProps, existingRevProps);
}
handlePropertesCopied(filteredCount > 0, revision);
return normalizedProps;
}
private SessionInfo openSourceRepository(SVNRepository targetRepos) throws SVNException {
SVNPropertyValue fromURL = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL);
SVNPropertyValue fromUUID = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID);
SVNPropertyValue 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, SVNLogType.FSFS);
}
SVNURL srcURL = SVNURL.parseURIEncoded(fromURL.getString());
SVNRepository srcRepos = createRepository(srcURL, fromUUID.getString(), false);
try {
return new SessionInfo(srcRepos, Long.parseLong(lastMergedRev.getString()));
} catch (NumberFormatException nfe) {
SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.IO_ERROR, nfe), SVNLogType.FSFS);
}
return null;
}
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, SVNLogType.FSFS);
}
}
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, SVNLogType.FSFS);
}
if (hostName.length() > 256) {
hostName = hostName.substring(0, 256);
}
String lockToken = hostName + ":" + SVNUUIDGenerator.formatUUID(SVNUUIDGenerator.generateUUID());
int i = 0;
SVNErrorMessage childError = null;
for (i = 0; i < LOCK_RETRY_COUNT; i++) {
checkCancelled();
SVNPropertyValue reposLockToken = repos.getRevisionPropertyValue(0, SVNRevisionProperty.LOCK);
if (reposLockToken != null) {
if (lockToken.equals(reposLockToken.getString())) {
return;
}
childError = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
"Failed to get lock on destination repos, currently held by ''{0}''", reposLockToken.getString());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//
}
} else if (i < LOCK_RETRY_COUNT - 1) {
repos.setRevisionPropertyValue(0, SVNRevisionProperty.LOCK, SVNPropertyValue.create(lockToken));
}
}
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
"Couldn''t get lock on destination repos after {0} attempts", String.valueOf(i));
if (childError != null) {
err.setChildErrorMessage(childError);
}
SVNErrorManager.error(err, SVNLogType.FSFS);
}
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;
}
}
public static SVNProperties normalizeRevisionProperties(SVNProperties revProps) throws SVNException {
SVNProperties normalizedProps = new SVNProperties();
for (Iterator propNamesIter = revProps.nameSet().iterator(); propNamesIter.hasNext();) {
String propName = (String) propNamesIter.next();
if (SVNPropertiesManager.propNeedsTranslation(propName)) {
SVNPropertyValue value = revProps.getSVNPropertyValue(propName);
String normalizedValue = normalizeString(SVNPropertyValue.getPropertyAsString(value));
if (normalizedValue != null) {
normalizedProps.put(propName, SVNPropertyValue.create(normalizedValue));
}
}
}
revProps.putAll(normalizedProps);
return normalizedProps;
}
public static String normalizeString(String string) throws SVNException {
if (string != null && string.indexOf(SVNProperty.EOL_CR_BYTES[0]) != -1) {
return SVNTranslator.translateString(string, SVNProperty.EOL_LF_BYTES, null, true, false);
}
return null;
}
}