/*
* ====================================================================
* 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;
import java.io.File;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.tmatesoft.svn.core.SVNCancelException;
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.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNHashSet;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.ISVNFileFetcher;
import org.tmatesoft.svn.core.internal.wc.SVNAmbientDepthFilterEditor;
import org.tmatesoft.svn.core.internal.wc.SVNCancellableEditor;
import org.tmatesoft.svn.core.internal.wc.SVNCancellableOutputStream;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
import org.tmatesoft.svn.core.internal.wc.SVNExportEditor;
import org.tmatesoft.svn.core.internal.wc.SVNExternal;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNPropertiesManager;
import org.tmatesoft.svn.core.internal.wc.SVNUpdateEditor;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminAreaFactory;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminAreaInfo;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNReporter;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.internal.wc.admin.SVNVersionedProperties;
import org.tmatesoft.svn.core.internal.wc.admin.SVNWCAccess;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.ISVNReporter;
import org.tmatesoft.svn.core.io.ISVNReporterBaton;
import org.tmatesoft.svn.core.io.SVNCapability;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;
/**
* This class provides methods which allow to check out, update, switch and relocate a
* Working Copy as well as export an unversioned directory or file from a repository.
*
* <p>
* Here's a list of the <b>SVNUpdateClient</b>'s methods
* matched against corresponing commands of the SVN command line
* client:
*
* <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>doCheckout()</td><td>'svn checkout'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doUpdate()</td><td>'svn update'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doSwitch()</td><td>'svn switch'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doRelocate()</td><td>'svn switch --relocate oldURL newURL'</td>
* </tr>
* <tr bgcolor="#EAEAEA" align="left">
* <td>doExport()</td><td>'svn export'</td>
* </tr>
* </table>
*
* @version 1.2
* @author TMate Software Ltd.
* @see <a target="_top" href="http://svnkit.com/kb/examples/">Examples</a>
*/
public class SVNUpdateClient extends SVNBasicClient {
private ISVNExternalsHandler myExternalsHandler;
private boolean myIsUpdateLocksOnDemand;
private boolean myIsExportExpandsKeywords;
/**
* Constructs and initializes an <b>SVNUpdateClient</b> object
* with the specified run-time configuration and authentication
* drivers.
*
* <p>
* If <code>options</code> is <span class="javakeyword">null</span>,
* then this <b>SVNUpdateClient</b> will be using a default run-time
* configuration driver which takes client-side settings from the
* default SVN's run-time configuration area but is not able to
* change those settings (read more on {@link ISVNOptions} and {@link SVNWCUtil}).
*
* <p>
* If <code>authManager</code> is <span class="javakeyword">null</span>,
* then this <b>SVNUpdateClient</b> will be using a default authentication
* and network layers driver (see {@link SVNWCUtil#createDefaultAuthenticationManager()})
* which uses server-side settings and auth storage from the
* default SVN's run-time configuration area (or system properties
* if that area is not found).
*
* @param authManager an authentication and network layers driver
* @param options a run-time configuration options driver
*/
public SVNUpdateClient(ISVNAuthenticationManager authManager, ISVNOptions options) {
super(authManager, options);
myIsExportExpandsKeywords = true;
}
/**
* Constructs and initializes an <b>SVNUpdateClient</b> object
* with the specified run-time configuration and authentication
* drivers.
*
* <p/>
* If <code>options</code> is <span class="javakeyword">null</span>,
* then this <b>SVNUpdateClient</b> will be using a default run-time
* configuration driver which takes client-side settings from the
* default SVN's run-time configuration area but is not able to
* change those settings (read more on {@link ISVNOptions} and {@link SVNWCUtil}).
*
* <p/>
* If <code>repositoryPool</code> is <span class="javakeyword">null</span>,
* then {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory} will be used to create {@link SVNRepository repository access objects}.
*
* @param repositoryPool a repository pool object
* @param options a run-time configuration options driver
*/
public SVNUpdateClient(ISVNRepositoryPool repositoryPool, ISVNOptions options) {
super(repositoryPool, options);
myIsExportExpandsKeywords = true;
}
/**
* Sets an externals handler to be used by this client object.
*
* @param externalsHandler user's implementation of {@link ISVNExternalsHandler}
* @see #getExternalsHandler()
* @since 1.2
*/
public void setExternalsHandler(ISVNExternalsHandler externalsHandler) {
myExternalsHandler = externalsHandler;
}
/**
* Returns an externals handler used by this update client.
*
* <p/>
* If no user's handler is provided then {@link ISVNExternalsHandler#DEFAULT} is returned and
* used by this client object by default.
*
* <p/>
* For more information what externals handlers are for, please, refer to {@link ISVNExternalsHandler}.
*
* @return externals handler being in use
* @see #setExternalsHandler(ISVNExternalsHandler)
* @since 1.2
*/
public ISVNExternalsHandler getExternalsHandler() {
if (myExternalsHandler == null) {
myExternalsHandler = ISVNExternalsHandler.DEFAULT;
}
return myExternalsHandler;
}
/**
* Brings the Working Copy item up-to-date with repository changes at the specified
* revision.
*
* <p>
* As a revision <b>SVNRevision</b>'s pre-defined constant fields can be used. For example,
* to update the Working Copy to the latest revision of the repository use
* {@link SVNRevision#HEAD HEAD}.
*
* @param file the Working copy item to be updated
* @param revision the desired revision against which the item will be updated
* @param recursive if <span class="javakeyword">true</span> and <code>file</code> is
* a directory then the entire tree will be updated, otherwise if
* <span class="javakeyword">false</span> - only items located immediately
* in the directory itself
* @return the revision number to which <code>file</code> was updated to
* @throws SVNException
* @deprecated use {@link #doUpdate(File, SVNRevision, SVNDepth, boolean, boolean)} instead
*/
public long doUpdate(File file, SVNRevision revision, boolean recursive) throws SVNException {
return doUpdate(file, revision, SVNDepth.fromRecurse(recursive), false, false);
}
/**
* @param file
* @param revision
* @param recursive
* @param force
* @return actual revision number
* @throws SVNException
* @deprecated use {@link #doUpdate(File, SVNRevision, SVNDepth, boolean, boolean)} instead
*/
public long doUpdate(File file, SVNRevision revision, boolean recursive, boolean force) throws SVNException {
return doUpdate(file, revision, SVNDepth.fromRecurse(recursive), force, false);
}
/**
* Updates working trees <code>paths</code> to <code>revision</code>.
* Unversioned paths that are direct children of a versioned path will cause an update that
* attempts to add that path, other unversioned paths are skipped.
*
* <p/>
* <code>revision</code> must represent a valid revision number ({@link SVNRevision#getNumber()} >= 0),
* or date ({@link SVNRevision#getDate()} != <span class="javakeyword">true</span>), or be equal to
* {@link SVNRevision#HEAD}. If <code>revision</code> does not meet these requirements, an exception with
* the error code {@link SVNErrorCode#CLIENT_BAD_REVISION} is thrown.
*
* <p/>
* The paths in <code>paths</code> can be from multiple working copies from multiple
* repositories, but even if they all come from the same repository there
* is no guarantee that revision represented by {@link SVNRevision#HEAD}
* will remain the same as each path is updated.
*
* <p/>
* If externals are {@link #isIgnoreExternals() ignored}, doesn't process externals definitions
* as part of this operation.
*
* <p/>
* If <code>depth</code> is {@link SVNDepth#INFINITY}, updates fully recursively.
* Else if it is {@link SVNDepth#IMMEDIATES} or {@link SVNDepth#FILES}, updates
* each target and its file entries, but not its subdirectories. Else if {@link SVNDepth#EMPTY},
* updates exactly each target, nonrecursively (essentially, updates the target's properties).
*
* <p/>
* If <code>depth</code> is {@link SVNDepth#UNKNOWN}, takes the working depth from
* <code>paths</code> and then behaves as described above.
*
* <p/>
* If <code>depthIsSticky</code> is set and <code>depth</code> is not {@link SVNDepth#UNKNOWN},
* then in addition to updating <code>paths</code>, also sets
* their sticky ambient depth value to <code>depth</codes>.
*
* <p/>
* If <code>allowUnversionedObstructions</code> is <span class="javakeyword">true</span> then the update
* tolerates existing unversioned items that obstruct added paths. Only obstructions of the same type
* (file or dir) as the added item are tolerated. The text of obstructing files is left as-is, effectively
* treating it as a user modification after the update. Working properties of obstructing items are set
* equal to the base properties. If <code>allowUnversionedObstructions</code> is
* <span class="javakeyword">false</span> then the update will abort if there are any unversioned
* obstructing items.
*
* <p/>
* If the caller's {@link ISVNEventHandler} is non-<span class="javakeyword">null</span>, it is invoked for
* each item handled by the update, and also for files restored from text-base. Also
* {@link ISVNEventHandler#checkCancelled()} will be used at various places during the update to check
* whether the caller wants to stop the update.
*
* <p/>
* Before updating a next path from <code>paths</code> this method calls {@link #handlePathListItem(File)}
* passing the path to it.
*
* <p/>
* This operation requires repository access (in case the repository is not on the same machine, network
* connection is established).
*
* @param paths working copy paths
* @param revision revision to update to
* @param depth tree depth to update
* @param allowUnversionedObstructions flag that allows tollerating unversioned items
* during update
* @param depthIsSticky flag that controls whether the requested depth
* should be written to the working copy
* @return an array of <code>long</code> revisions with each
* element set to the revision to which <code>revision</code> was resolved
* @throws SVNException
* @since 1.2, SVN 1.5
*/
public long[] doUpdate(File[] paths, SVNRevision revision, SVNDepth depth, boolean allowUnversionedObstructions,
boolean depthIsSticky) throws SVNException {
if (paths == null) {
return new long[0];
}
Collection revisions = new LinkedList();
for (int i = 0; i < paths.length; i++) {
checkCancelled();
File path = paths[i];
try {
setEventPathPrefix("");
handlePathListItem(path);
long rev = doUpdate(path, revision, depth, allowUnversionedObstructions, depthIsSticky);
revisions.add(new Long(rev));
} catch (SVNException svne) {
if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.WC_NOT_DIRECTORY) {
SVNEvent skipEvent = SVNEventFactory.createSVNEvent(path, SVNNodeKind.UNKNOWN, null, SVNRepository.INVALID_REVISION, SVNEventAction.SKIP, SVNEventAction.UPDATE_COMPLETED, null, null);
dispatchEvent(skipEvent);
revisions.add(new Long(-1));
continue;
}
throw svne;
} finally {
setEventPathPrefix(null);
}
}
sleepForTimeStamp();
long[] result = new long[revisions.size()];
int i = 0;
for (Iterator revs = revisions.iterator(); revs.hasNext();) {
Long value = (Long) revs.next();
result[i++] = value.longValue();
}
return result;
}
/**
* Updates working copy <code></code> to <code>revision</code>.
* Unversioned paths that are direct children of a versioned path will cause an update that
* attempts to add that path, other unversioned paths are skipped.
*
* <p/>
* <code>revision</code> must represent a valid revision number ({@link SVNRevision#getNumber()} >= 0),
* or date ({@link SVNRevision#getDate()} != <span class="javakeyword">true</span>), or be equal to
* {@link SVNRevision#HEAD}. If <code>revision</code> does not meet these requirements, an exception with
* the error code {@link SVNErrorCode#CLIENT_BAD_REVISION} is thrown.
*
* <p/>
* If externals are {@link #isIgnoreExternals() ignored}, doesn't process externals definitions
* as part of this operation.
*
* <p/>
* If <code>depth</code> is {@link SVNDepth#INFINITY}, updates fully recursively.
* Else if it is {@link SVNDepth#IMMEDIATES} or {@link SVNDepth#FILES}, updates
* <code>path</code> and its file entries, but not its subdirectories. Else if {@link SVNDepth#EMPTY},
* updates exactly <code>path</code>, nonrecursively (essentially, updates the target's properties).
*
* <p/>
* If <code>depth</code> is {@link SVNDepth#UNKNOWN}, takes the working depth from
* <code>path</code> and then behaves as described above.
*
* <p/>
* If <code>depthIsSticky</code> is set and <code>depth</code> is not {@link SVNDepth#UNKNOWN},
* then in addition to updating <code>path</code>, also sets its sticky ambient depth value to
* <code>depth</codes>.
*
* <p/>
* If <code>allowUnversionedObstructions</code> is <span class="javakeyword">true</span> then the update
* tolerates existing unversioned items that obstruct added paths. Only obstructions of the same type
* (file or dir) as the added item are tolerated. The text of obstructing files is left as-is, effectively
* treating it as a user modification after the update. Working properties of obstructing items are set
* equal to the base properties. If <code>allowUnversionedObstructions</code> is
* <span class="javakeyword">false</span> then the update will abort if there are any unversioned
* obstructing items.
*
* <p/>
* If the caller's {@link ISVNEventHandler} is non-<span class="javakeyword">null</span>, it is invoked for
* each item handled by the update, and also for files restored from text-base. Also
* {@link ISVNEventHandler#checkCancelled()} will be used at various places during the update to check
* whether the caller wants to stop the update.
*
* <p/>
* This operation requires repository access (in case the repository is not on the same machine, network
* connection is established).
*
* @param path working copy path
* @param revision revision to update to
* @param depth tree depth to update
* @param allowUnversionedObstructions flag that allows tollerating unversioned items
* during update
* @param depthIsSticky flag that controls whether the requested depth
* should be written to the working copy
* @return revision to which <code>revision</code> was resolved
* @throws SVNException
* @since 1.2, SVN 1.5
*/
public long doUpdate(File path, SVNRevision revision, SVNDepth depth, boolean allowUnversionedObstructions, boolean depthIsSticky) throws SVNException {
return update(path, revision, depth, allowUnversionedObstructions, depthIsSticky, true);
}
private long update(File path, SVNRevision revision, SVNDepth depth, boolean allowUnversionedObstructions, boolean depthIsSticky, boolean sendCopyFrom) throws SVNException {
depth = depth == null ? SVNDepth.UNKNOWN : depth;
if (depth == SVNDepth.UNKNOWN) {
depthIsSticky = false;
}
path = path.getAbsoluteFile();
SVNWCAccess wcAccess = createWCAccess();
SVNAdminAreaInfo adminInfo = null;
int admOpenDepth = getLevelsToLockFromDepth(depth);
try {
if (isUpdateLocksOnDemand()) {
wcAccess.openAnchor(path, true, 0);
wcAccess.close();
}
adminInfo = wcAccess.openAnchor(path, !isUpdateLocksOnDemand(), admOpenDepth);
SVNAdminArea anchorArea = adminInfo.getAnchor();
SVNEntry entry = anchorArea.getEntry(anchorArea.getThisDirName(), false);
SVNURL url = entry.getSVNURL();
if (url == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL,
"Entry ''{0}'' has no URL", anchorArea.getRoot());
SVNErrorManager.error(err, SVNLogType.WC);
}
String[] preservedExts = getOptions().getPreservedConflictFileExtensions();
SVNRepository repos = createRepository(url, anchorArea.getRoot(), wcAccess, true);
boolean serverSupportsDepth = repos.hasCapability(SVNCapability.DEPTH);
final SVNReporter reporter = new SVNReporter(adminInfo, path, true, !serverSupportsDepth,
depth, isUpdateLocksOnDemand(), false, getDebugLog());
String target = "".equals(adminInfo.getTargetName()) ? null : adminInfo.getTargetName();
long revNumber = getRevisionNumber(revision, repos, path);
SVNURL reposRoot = repos.getRepositoryRoot(true);
wcAccess.setRepositoryRoot(path, reposRoot);
final SVNRepository repos2 = createRepository(reposRoot, null, null, false);
ISVNFileFetcher fileFetcher = new ISVNFileFetcher() {
public long fetchFile(String path, long revision, OutputStream os, SVNProperties properties) throws SVNException {
return repos2.getFile(path, revision, properties, os);
}
};
SVNUpdateEditor editor = SVNUpdateEditor.createUpdateEditor(adminInfo, null, allowUnversionedObstructions,
depthIsSticky, depth, preservedExts, fileFetcher, isUpdateLocksOnDemand());
ISVNEditor filterEditor = SVNAmbientDepthFilterEditor.wrap(editor, adminInfo, depth, depthIsSticky);
try {
repos.update(revNumber, target, depth, sendCopyFrom, reporter, SVNCancellableEditor.newInstance(filterEditor, this, getDebugLog()));
} finally {
repos2.closeSession();
}
long targetRevision = editor.getTargetRevision();
if (targetRevision >= 0) {
if ((depth == SVNDepth.INFINITY || depth == SVNDepth.UNKNOWN) && !isIgnoreExternals()) {
handleExternals(adminInfo.getAnchor().getRoot(),
adminInfo.getOldExternals(), adminInfo.getNewExternals(), adminInfo.getDepths(), url, reposRoot, depth, false, true);
}
dispatchEvent(SVNEventFactory.createSVNEvent(adminInfo.getTarget().getRoot(),
SVNNodeKind.NONE, null, targetRevision, SVNEventAction.UPDATE_COMPLETED, null, null,
null, reporter.getReportedFilesCount(), reporter.getTotalFilesCount()));
}
return targetRevision;
} finally {
wcAccess.close();
sleepForTimeStamp();
}
}
/**
* Sets whether working copies should be locked on demand or not during an update process.
*
* <p>
* For additional description, please, refer to {@link #isUpdateLocksOnDemand()}.
*
* @param locksOnDemand <span class="javakeyword">true</span> to make update lock a working copy tree on
* demand only (for those subdirectories only which will be changed by update)
*/
public void setUpdateLocksOnDemand(boolean locksOnDemand) {
myIsUpdateLocksOnDemand = locksOnDemand;
}
/**
* Says whether the entire working copy should be locked while updating or not.
*
* <p/>
* If this method returns <span class="javakeyword">false</span>, then the working copy will be
* closed for all paths involved in the update. Otherwise only those working copy subdirectories
* will be locked, which will be either changed by the update or which contain deleted files
* that should be restored during the update; all other versioned subdirectories than won't be
* touched by the update will remain opened for read only access without locking.
*
* <p/>
* Locking working copies on demand is intended to improve update performance for large working
* copies because even a no-op update on a huge working copy always locks the entire tree by default.
* And locking a working copy tree means opening special lock files for privileged access for all
* subdirectories involved. This makes an update process work slower. Locking wc on demand
* feature suggests such a workaround to enhance update performance.
*
* @return <span class="javakeyword">true</span> when locking wc on demand
*/
public boolean isUpdateLocksOnDemand() {
return myIsUpdateLocksOnDemand;
}
/**
* Updates the Working Copy item to mirror a new URL.
*
* <p>
* As a revision <b>SVNRevision</b>'s pre-defined constant fields can be used. For example,
* to update the Working Copy to the latest revision of the repository use
* {@link SVNRevision#HEAD HEAD}.
*
* <p>
* Calling this method is equivalent to
* <code>doSwitch(file, url, SVNRevision.UNDEFINED, revision, recursive)</code>.
*
* @param file the Working copy item to be switched
* @param url the repository location as a target against which the item will
* be switched
* @param revision the desired revision of the repository target
* @param recursive if <span class="javakeyword">true</span> and <code>file</code> is
* a directory then the entire tree will be updated, otherwise if
* <span class="javakeyword">false</span> - only items located immediately
* in the directory itself
* @return the revision number to which <code>file</code> was updated to
* @throws SVNException
* @deprecated use {@link #doSwitch(File, SVNURL, SVNRevision, SVNRevision, SVNDepth, boolean, boolean)} instead
*/
public long doSwitch(File file, SVNURL url, SVNRevision revision, boolean recursive) throws SVNException {
return doSwitch(file, url, SVNRevision.UNDEFINED, revision, SVNDepth.getInfinityOrFilesDepth(recursive),
false, false);
}
/**
* Updates the Working Copy item to mirror a new URL.
*
* <p>
* As a revision <b>SVNRevision</b>'s pre-defined constant fields can be used. For example,
* to update the Working Copy to the latest revision of the repository use
* {@link SVNRevision#HEAD HEAD}.
*
* @param file the Working copy item to be switched
* @param url the repository location as a target against which the item will
* be switched
* @param pegRevision a revision in which <code>file</code> is first looked up
* in the repository
* @param revision the desired revision of the repository target
* @param recursive if <span class="javakeyword">true</span> and <code>file</code> is
* a directory then the entire tree will be updated, otherwise if
* <span class="javakeyword">false</span> - only items located immediately
* in the directory itself
* @return the revision number to which <code>file</code> was updated to
* @throws SVNException
* @deprecated use {@link #doSwitch(File, SVNURL, SVNRevision, SVNRevision, SVNDepth, boolean, boolean)} instead
*/
public long doSwitch(File file, SVNURL url, SVNRevision pegRevision, SVNRevision revision, boolean recursive) throws SVNException {
return doSwitch(file, url, pegRevision, revision, SVNDepth.getInfinityOrFilesDepth(recursive), false,
false);
}
/**
* @param file
* @param url
* @param pegRevision
* @param revision
* @param recursive
* @param force
* @return actual revision number
* @throws SVNException
* @deprecated use {@link #doSwitch(File, SVNURL, SVNRevision, SVNRevision, SVNDepth, boolean, boolean)} instead
*/
public long doSwitch(File file, SVNURL url, SVNRevision pegRevision, SVNRevision revision, boolean recursive, boolean force) throws SVNException {
return doSwitch(file, url, pegRevision, revision, SVNDepth.getInfinityOrFilesDepth(recursive), force, false);
}
/**
* Switches working tree <code>path</code> to <code>url</code>\<code>pegRevision</code> at
* <code>revision</code>.
*
* <p/>
* Summary of purpose: this is normally used to switch a working
* directory over to another line of development, such as a branch or
* a tag. Switching an existing working directory is more efficient
* than checking out <code>url</code> from scratch.
*
* <p/>
* <code>revision</code> must represent a valid revision number ({@link SVNRevision#getNumber()} >= 0),
* or date ({@link SVNRevision#getDate()} != <span class="javakeyword">true</span>), or be equal to
* {@link SVNRevision#HEAD}. If <code>revision</code> does not meet these requirements, an exception with
* the error code {@link SVNErrorCode#CLIENT_BAD_REVISION} is thrown.
*
* <p/>
* If <code>depth</code> is {@link SVNDepth#INFINITY}, switches fully recursively.
* Else if it is {@link SVNDepth#IMMEDIATES}, switches <code>path</code> and its file
* children (if any), and switches subdirectories but does not update
* them. Else if {@link SVNDepth#FILES}, switches just file children,
* ignoring subdirectories completely. Else if {@link SVNDepth#EMPTY},
* switches just <code>path</code> and touches nothing underneath it.
*
* <p/>
* If <code>depthIsSticky</code> is set and <code>depth</code> is not
* {@link SVNDepth#UNKNOWN}, then in addition to switching <code>path</code>, also sets
* its sticky ambient depth value to <code>depth</code>.
*
* <p/>
* If externals are {@link #isIgnoreExternals() ignored}, doesn't process externals definitions
* as part of this operation.
*
* <p/>
* If <code>allowUnversionedObstructions</code> is <span class="javakeyword">true</span> then the switch
* tolerates existing unversioned items that obstruct added paths. Only
* obstructions of the same type (file or dir) as the added item are
* tolerated. The text of obstructing files is left as-is, effectively
* treating it as a user modification after the switch. Working
* properties of obstructing items are set equal to the base properties.
* If <code>allowUnversionedObstructions</code> is <span class="javakeyword">false</span> then the switch
* will abort if there are any unversioned obstructing items.
*
* <p/>
* If the caller's {@link ISVNEventHandler} is non-<span class="javakeyword">null</span>, it is invoked for
* paths affected by the switch, and also for files restored from text-base. Also
* {@link ISVNEventHandler#checkCancelled()} will be used at various places during the switch to check
* whether the caller wants to stop the switch.
*
* <p/>
* This operation requires repository access (in case the repository is not on the same machine, network
* connection is established).
*
* @param path the Working copy item to be switched
* @param url the repository location as a target against which the item will
* be switched
* @param pegRevision a revision in which <code>path</code> is first looked up
* in the repository
* @param revision the desired revision of the repository target
* @param depth tree depth to update
* @param allowUnversionedObstructions flag that allows tollerating unversioned items
* during update
* @param depthIsSticky flag that controls whether the requested depth
* should be written into the working copy
* @return value of the revision to which the working copy was actually switched
* @throws SVNException
* @since 1.2, SVN 1.5
*/
public long doSwitch(File path, SVNURL url, SVNRevision pegRevision, SVNRevision revision, SVNDepth depth,
boolean allowUnversionedObstructions, boolean depthIsSticky) throws SVNException {
SVNWCAccess wcAccess = createWCAccess();
try {
SVNAdminAreaInfo info = wcAccess.openAnchor(path, true, SVNWCAccess.INFINITE_DEPTH);
final SVNReporter reporter = new SVNReporter(info, path, true, false, depth, false, false, getDebugLog());
SVNAdminArea anchorArea = info.getAnchor();
SVNEntry entry = anchorArea.getVersionedEntry(anchorArea.getThisDirName(), false);
SVNURL sourceURL = entry.getSVNURL();
if (sourceURL == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "Directory ''{0}'' has no URL", anchorArea.getRoot());
SVNErrorManager.error(err, SVNLogType.WC);
}
long[] revs = new long[1];
// should fail on missing repository.
SVNRepository repository = createRepository(url, null, anchorArea, pegRevision, revision, revs);
long revNumber = revs[0];
url = repository.getLocation();
// root of the switched repos.
SVNURL sourceRoot = repository.getRepositoryRoot(true);
if (!SVNPathUtil.isAncestor(sourceRoot.toString(), sourceURL.toString())) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SWITCH, "''{0}''\nis not the same repository as\n''{1}''",
new Object[] {url.toString(), sourceRoot.toString()});
SVNErrorManager.error(err, SVNLogType.WC);
}
// reparent to the sourceURL
repository.setLocation(sourceURL, false);
String[] preservedExts = getOptions().getPreservedConflictFileExtensions();
SVNUpdateEditor editor = SVNUpdateEditor.createUpdateEditor(info, url.toString(),
allowUnversionedObstructions, depthIsSticky, depth, preservedExts, null, false);
ISVNEditor filterEditor = SVNAmbientDepthFilterEditor.wrap(editor, info, depth, depthIsSticky);
String target = "".equals(info.getTargetName()) ? null : info.getTargetName();
repository.update(url, revNumber, target, depth, reporter, SVNCancellableEditor.newInstance(filterEditor, this, getDebugLog()));
long targetRevision = editor.getTargetRevision();
if (targetRevision >= 0 && !isIgnoreExternals() && depth.isRecursive()) {
handleExternals(info.getAnchor().getRoot(), info.getOldExternals(), info.getNewExternals(),
info.getDepths(), url, sourceRoot, depth, false, false);
}
dispatchEvent(SVNEventFactory.createSVNEvent(info.getTarget().getRoot(), SVNNodeKind.NONE, null,
targetRevision, SVNEventAction.UPDATE_COMPLETED, null, null, null, reporter.getReportedFilesCount(), reporter.getTotalFilesCount()));
return targetRevision;
} finally {
wcAccess.close();
sleepForTimeStamp();
}
}
/**
* Checks out a Working Copy from a repository.
*
* <p>
* If the destination path (<code>dstPath</code>) is <span class="javakeyword">null</span>
* then the last component of <code>url</code> is used for the local directory name.
*
* <p>
* As a revision <b>SVNRevision</b>'s pre-defined constant fields can be used. For example,
* to check out a Working Copy at the latest revision of the repository use
* {@link SVNRevision#HEAD HEAD}.
*
* @param url a repository location from where a Working Copy will be checked out
* @param dstPath the local path where the Working Copy will be placed
* @param pegRevision the revision at which <code>url</code> will be firstly seen
* in the repository to make sure it's the one that is needed
* @param revision the desired revision of the Working Copy to be checked out
* @param recursive if <span class="javakeyword">true</span> and <code>url</code> is
* a directory then the entire tree will be checked out, otherwise if
* <span class="javakeyword">false</span> - only items located immediately
* in the directory itself
* @return the revision number of the Working Copy
* @throws SVNException <code>url</code> refers to a file, not a directory; <code>dstPath</code>
* already exists but it is a file, not a directory; <code>dstPath</code> already
* exists and is a versioned directory but has a different URL (repository location
* against which the directory is controlled)
* @deprecated use {@link #doCheckout(SVNURL, File, SVNRevision, SVNRevision, SVNDepth, boolean)} instead
*/
public long doCheckout(SVNURL url, File dstPath, SVNRevision pegRevision, SVNRevision revision, boolean recursive) throws SVNException {
return doCheckout(url, dstPath, pegRevision, revision, SVNDepth.fromRecurse(recursive), false);
}
/**
* @param url
* @param dstPath
* @param pegRevision
* @param revision
* @param recursive
* @param force
* @return actual revision number
* @throws SVNException
* @deprecated use {@link #doCheckout(SVNURL, File, SVNRevision, SVNRevision, SVNDepth, boolean)} instead
*/
public long doCheckout(SVNURL url, File dstPath, SVNRevision pegRevision, SVNRevision revision, boolean recursive, boolean force) throws SVNException {
return doCheckout(url, dstPath, pegRevision, revision, SVNDepth.fromRecurse(recursive), force);
}
/**
* Checks out a working copy of <code>url</code> at <code>revision</code>, looked up at
* <code>pegRevision</code>, using <code>dstPath</code> as the root directory of the newly
* checked out working copy.
*
* <p/>
* If <code>pegRevision</code> is {@link SVNRevision#UNDEFINED}, then it
* defaults to {@link SVNRevision#HEAD}.
*
* <p/>
* <code>revision</code> must represent a valid revision number ({@link SVNRevision#getNumber()} >= 0),
* or date ({@link SVNRevision#getDate()} != <span class="javakeyword">true</span>), or be equal to
* {@link SVNRevision#HEAD}. If <code>revision</code> does not meet these requirements, an exception with
* the error code {@link SVNErrorCode#CLIENT_BAD_REVISION} is thrown.
*
* <p/>
* If <code>depth</code> is {@link SVNDepth#INFINITY}, checks out fully recursively.
* Else if it is {@link SVNDepth#IMMEDIATES}, checks out <code>url</code> and its
* immediate entries (subdirectories will be present, but will be at
* depth {@link SVNDepth#EMPTY} themselves); else {@link SVNDepth#FILES},
* checks out <code>url</code> and its file entries, but no subdirectories; else
* if {@link SVNDepth#EMPTY}, checks out <code>url</code> as an empty directory at
* that depth, with no entries present.
*
* <p/>
* If <code>depth</code> is {@link SVNDepth#UNKNOWN}, then behave as if for
* {@link SVNDepth#INFINITY}, except in the case of resuming a previous
* checkout of <code>dstPath</code> (i.e., updating), in which case uses the depth
* of the existing working copy.
*
* <p/>
* If externals are {@link #isIgnoreExternals() ignored}, doesn't process externals definitions
* as part of this operation.
*
* <p/>
* If <code>allowUnversionedObstructions</code> is <span class="javakeyword">true</span> then the checkout
* tolerates existing unversioned items that obstruct added paths from <code>url</code>. Only
* obstructions of the same type (file or dir) as the added item are tolerated. The text of obstructing
* files is left as-is, effectively treating it as a user modification after the checkout. Working
* properties of obstructing items are set equal to the base properties. If
* <code>allowUnversionedObstructions</code> is <span class="javakeyword">false</span> then the checkout
* will abort if there are any unversioned obstructing items.
*
* <p/>
* If the caller's {@link ISVNEventHandler} is non-<span class="javakeyword">null</span>, it is invoked
* as the checkout processes. Also {@link ISVNEventHandler#checkCancelled()} will be used at various places
* during the checkout to check whether the caller wants to stop the checkout.
*
* <p/>
* This operation requires repository access (in case the repository is not on the same machine, network
* connection is established).
*
* @param url a repository location from where a Working Copy will be checked out
* @param dstPath the local path where the Working Copy will be placed
* @param pegRevision the revision at which <code>url</code> will be firstly seen
* in the repository to make sure it's the one that is needed
* @param revision the desired revision of the Working Copy to be checked out
* @param depth tree depth
* @param allowUnversionedObstructions flag that allows tollerating unversioned items
* during
* @return value of the revision actually checked out from the repository
* @throws SVNException <ul>
* <li/>{@link SVNErrorCode#UNSUPPORTED_FEATURE} - if <code>url</code> refers to a
* file rather than a directory
* <li/>{@link SVNErrorCode#RA_ILLEGAL_URL} - if <code>url</code> does not exist
* </ul>
* @since 1.2, SVN 1.5
*/
public long doCheckout(SVNURL url, File dstPath, SVNRevision pegRevision, SVNRevision revision, SVNDepth depth,
boolean allowUnversionedObstructions) throws SVNException {
if (dstPath == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_FILENAME, "Checkout destination path can not be NULL");
SVNErrorManager.error(err, SVNLogType.WC);
}
pegRevision = pegRevision == null ? SVNRevision.UNDEFINED : pegRevision;
if (!revision.isValid() && pegRevision.isValid()) {
revision = pegRevision;
}
if (!revision.isValid()) {
revision = SVNRevision.HEAD;
}
SVNRepository repos = createRepository(url, null, null, pegRevision, revision, null);
url = repos.getLocation();
long revNumber = getRevisionNumber(revision, repos, null);
SVNNodeKind targetNodeKind = repos.checkPath("", revNumber);
if (targetNodeKind == SVNNodeKind.FILE) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "URL ''{0}'' refers to a file, not a directory", url);
SVNErrorManager.error(err, SVNLogType.WC);
} else if (targetNodeKind == SVNNodeKind.NONE) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_ILLEGAL_URL, "URL ''{0}'' doesn''t exist", url);
SVNErrorManager.error(err, SVNLogType.WC);
}
String uuid = repos.getRepositoryUUID(true);
SVNURL repositoryRoot = repos.getRepositoryRoot(true);
long result = -1;
depth = depth == null ? SVNDepth.UNKNOWN : depth;
SVNWCAccess wcAccess = createWCAccess();
SVNFileType kind = SVNFileType.getType(dstPath);
if (kind == SVNFileType.NONE) {
depth = depth == SVNDepth.UNKNOWN ? SVNDepth.INFINITY : depth;
SVNAdminAreaFactory.createVersionedDirectory(dstPath, url, repositoryRoot, uuid, revNumber, depth);
result = update(dstPath, revision, depth, allowUnversionedObstructions, true, false);
} else if (kind == SVNFileType.DIRECTORY) {
int formatVersion = SVNAdminAreaFactory.checkWC(dstPath, true);
if (formatVersion != 0) {
SVNAdminArea adminArea = wcAccess.open(dstPath, false, 0);
SVNEntry rootEntry = adminArea.getEntry(adminArea.getThisDirName(), false);
wcAccess.closeAdminArea(dstPath);
if (rootEntry.getSVNURL() != null && url.equals(rootEntry.getSVNURL())) {
result = update(dstPath, revision, depth, allowUnversionedObstructions, true, false);
} else {
String message = "''{0}'' is already a working copy for a different URL";
if (rootEntry.isIncomplete()) {
message += "; perform update to complete it";
}
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE, message, dstPath);
SVNErrorManager.error(err, SVNLogType.WC);
}
} else {
depth = depth == SVNDepth.UNKNOWN ? SVNDepth.INFINITY : depth;
SVNAdminAreaFactory.createVersionedDirectory(dstPath, url, repositoryRoot, uuid, revNumber, depth);
result = update(dstPath, revision, depth, allowUnversionedObstructions, true, false);
}
} else {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NODE_KIND_CHANGE, "''{0}'' already exists and is not a directory", dstPath);
SVNErrorManager.error(err, SVNLogType.WC);
}
return result;
}
/**
* Exports a clean directory or single file from a repository.
*
* <p>
* If <code>eolStyle</code> is not <span class="javakeyword">null</span> then it should denote
* a specific End-Of-Line marker for the files to be exported. Significant values for
* <code>eolStyle</code> are:
* <ul>
* <li>"CRLF" (Carriage Return Line Feed) - this causes files to contain '\r\n' line ending sequences
* for EOL markers, regardless of the operating system in use (for instance, this EOL marker is used by
* software on the Windows platform).
* <li>"LF" (Line Feed) - this causes files to contain '\n' line ending sequences
* for EOL markers, regardless of the operating system in use (for instance, this EOL marker is used by
* software on the Unix platform).
* <li>"CR" (Carriage Return) - this causes files to contain '\r' line ending sequences
* for EOL markers, regardless of the operating system in use (for instance, this EOL marker was used by
* software on older Macintosh platforms).
* <li>"native" - this causes files to contain the EOL markers that are native to the operating system
* on which SVNKit is run.
* </ul>
*
* @param url a repository location from where the unversioned directory/file will
* be exported
* @param dstPath the local path where the repository items will be exported to
* @param pegRevision the revision at which <code>url</code> will be firstly seen
* in the repository to make sure it's the one that is needed
* @param revision the desired revision of the directory/file to be exported
* @param eolStyle a string that denotes a specific End-Of-Line charecter;
* @param force <span class="javakeyword">true</span> to fore the operation even
* if there are local files with the same names as those in the repository
* (local ones will be replaced)
* @param recursive if <span class="javakeyword">true</span> and <code>url</code> is
* a directory then the entire tree will be exported, otherwise if
* <span class="javakeyword">false</span> - only items located immediately
* in the directory itself
* @return the revision number of the exported directory/file
* @throws SVNException
* @deprecated use {@link #doExport(SVNURL, File, SVNRevision, SVNRevision, String, boolean, SVNDepth)}
*/
public long doExport(SVNURL url, File dstPath, SVNRevision pegRevision, SVNRevision revision, String eolStyle,
boolean force, boolean recursive) throws SVNException {
return doExport(url, dstPath, pegRevision, revision, eolStyle, force, SVNDepth.fromRecurse(recursive));
}
/**
* Exports the contents of a subversion repository into a 'clean' directory (meaning a
* directory with no administrative directories).
*
* <p/>
* <code>pegRevision</code> is the revision where the path is first looked up.
* If <code>pegRevision</code> is {@link SVNRevision#UNDEFINED},
* then it defaults to {@link SVNRevision#HEAD}.
*
* <p/>
* If externals are {@link #isIgnoreExternals() ignored}, doesn't process externals definitions
* as part of this operation.
*
* <p/>
* <code>eolStyle</code> allows you to override the standard eol marker on the platform
* you are running on. Can be either "LF", "CR" or "CRLF" or <span class="javakeyword">null</span>.
* If <span class="javakeyword">null</span> will use the standard eol marker. Any other value will cause
* an exception with the error code {@link SVNErrorCode#IO_UNKNOWN_EOL} error to be returned.
*
* <p>
* If <code>depth</code> is {@link SVNDepth#INFINITY}, exports fully recursively.
* Else if it is {@link SVNDepth#IMMEDIATES}, exports <code>url</code> and its immediate
* children (if any), but with subdirectories empty and at
* {@link SVNDepth#EMPTY}. Else if {@link SVNDepth#FILES}, exports <code>url</code> and
* its immediate file children (if any) only. If <code>depth</code> is {@link SVNDepth#EMPTY},
* then exports exactly <code>url</code> and none of its children.
*
* @param url repository url to export from
* @param dstPath path to export to
* @param pegRevision the revision at which <code>url</code> will be firstly seen
* in the repository to make sure it's the one that is needed
* @param revision the desired revision of the directory/file to be exported
* @param eolStyle a string that denotes a specific End-Of-Line charecter
* @param overwrite if <span class="javakeyword">true</span>, will cause the export to overwrite
* files or directories
* @param depth tree depth
* @return value of the revision actually exported
* @throws SVNException
* @since 1.2, SVN 1.5
*/
public long doExport(SVNURL url, File dstPath, SVNRevision pegRevision, SVNRevision revision, String eolStyle,
boolean overwrite, SVNDepth depth) throws SVNException {
long[] revNum = { SVNRepository.INVALID_REVISION };
SVNRepository repository = createRepository(url, null, null, pegRevision, revision, revNum);
long exportedRevision = doRemoteExport(repository, revNum[0], dstPath, eolStyle, overwrite, depth);
dispatchEvent(SVNEventFactory.createSVNEvent(null, SVNNodeKind.NONE, null, exportedRevision,
SVNEventAction.UPDATE_COMPLETED, null, null, null));
return exportedRevision;
}
/**
* Exports a clean directory or single file from eihter a source Working Copy or
* a repository.
*
* <p>
* How this method works:
* <ul>
* <li> If <code>revision</code> is different from {@link SVNRevision#BASE BASE},
* {@link SVNRevision#WORKING WORKING}, {@link SVNRevision#COMMITTED COMMITTED},
* {@link SVNRevision#UNDEFINED UNDEFINED} - then the repository origin of <code>srcPath</code>
* will be exported (what is done by "remote" {@link #doExport(SVNURL, File, SVNRevision, SVNRevision, String, boolean, boolean)
* doExport()}).
* <li> In other cases a clean unversioned copy of <code>srcPath</code> - either a directory or a single file -
* is exported to <code>dstPath</code>.
* </ul>
*
* <p>
* If <code>eolStyle</code> is not <span class="javakeyword">null</span> then it should denote
* a specific End-Of-Line marker for the files to be exported. Significant values for
* <code>eolStyle</code> are:
* <ul>
* <li>"CRLF" (Carriage Return Line Feed) - this causes files to contain '\r\n' line ending sequences
* for EOL markers, regardless of the operating system in use (for instance, this EOL marker is used by
* software on the Windows platform).
* <li>"LF" (Line Feed) - this causes files to contain '\n' line ending sequences
* for EOL markers, regardless of the operating system in use (for instance, this EOL marker is used by
* software on the Unix platform).
* <li>"CR" (Carriage Return) - this causes files to contain '\r' line ending sequences
* for EOL markers, regardless of the operating system in use (for instance, this EOL marker was used by
* software on older Macintosh platforms).
* <li>"native" - this causes files to contain the EOL markers that are native to the operating system
* on which SVNKit is run.
* </ul>
*
* @param srcPath a repository location from where the unversioned directory/file will
* be exported
* @param dstPath the local path where the repository items will be exported to
* @param pegRevision the revision at which <code>url</code> will be firstly seen
* in the repository to make sure it's the one that is needed
* @param revision the desired revision of the directory/file to be exported
* @param eolStyle a string that denotes a specific End-Of-Line charecter;
* @param force <span class="javakeyword">true</span> to fore the operation even
* if there are local files with the same names as those in the repository
* (local ones will be replaced)
* @param recursive if <span class="javakeyword">true</span> and <code>url</code> is
* a directory then the entire tree will be exported, otherwise if
* <span class="javakeyword">false</span> - only items located immediately
* in the directory itself
* @return the revision number of the exported directory/file
* @throws SVNException
* @deprecated use {@link #doExport(File, File, SVNRevision, SVNRevision, String, boolean, SVNDepth)}
*/
public long doExport(File srcPath, final File dstPath, SVNRevision pegRevision, SVNRevision revision,
String eolStyle, final boolean force, boolean recursive) throws SVNException {
return doExport(srcPath, dstPath, pegRevision, revision, eolStyle, force, SVNDepth.fromRecurse(recursive));
}
/**
* Exports the contents of either a subversion repository or a
* subversion working copy into a 'clean' directory (meaning a
* directory with no administrative directories).
*
* <p/>
* <code>pegRevision</code> is the revision where the path is first looked up
* when exporting from a repository. If <code>pegRevision</code> is {@link SVNRevision#UNDEFINED},
* then it defaults to {@link SVNRevision#WORKING}.
*
* <p/>
* If <code>revision</code> is one of:
* <ul>
* <li/>{@link SVNRevision#BASE}
* <li/>{@link SVNRevision#WORKING}
* <li/>{@link SVNRevision#COMMITTED}
* <li/>{@link SVNRevision#UNDEFINED}
* </ul>
* then local export is performed. Otherwise exporting from the repository.
*
* <p/>
* If externals are {@link #isIgnoreExternals() ignored}, doesn't process externals definitions
* as part of this operation.
*
* <p/>
* <code>eolStyle</code> allows you to override the standard eol marker on the platform
* you are running on. Can be either "LF", "CR" or "CRLF" or <span class="javakeyword">null</span>.
* If <span class="javakeyword">null</span> will use the standard eol marker. Any other value will cause
* an exception with the error code {@link SVNErrorCode#IO_UNKNOWN_EOL} error to be returned.
*
* <p>
* If <code>depth</code> is {@link SVNDepth#INFINITY}, exports fully recursively.
* Else if it is {@link SVNDepth#IMMEDIATES}, exports <code>srcPath</code> and its immediate
* children (if any), but with subdirectories empty and at
* {@link SVNDepth#EMPTY}. Else if {@link SVNDepth#FILES}, exports <code>srcPath</code> and
* its immediate file children (if any) only. If <code>depth</code> is {@link SVNDepth#EMPTY},
* then exports exactly <code>srcPath</code> and none of its children.
*
* @param srcPath working copy path
* @param dstPath path to export to
* @param pegRevision the revision at which <code>url</code> will be firstly seen
* in the repository to make sure it's the one that is needed
* @param revision the desired revision of the directory/file to be exported; used only
* when exporting from a repository
* @param eolStyle a string that denotes a specific End-Of-Line charecter
* @param overwrite if <span class="javakeyword">true</span>, will cause the export to overwrite
* files or directories
* @param depth tree depth
* @return value of the revision actually exported
* @throws SVNException
* @since 1.2, SVN 1.5
*/
public long doExport(File srcPath, final File dstPath, SVNRevision pegRevision, SVNRevision revision,
String eolStyle, final boolean overwrite, SVNDepth depth) throws SVNException {
long exportedRevision = -1;
if (revision != SVNRevision.BASE && revision != SVNRevision.WORKING && revision != SVNRevision.COMMITTED && revision != SVNRevision.UNDEFINED) {
SVNRepository repository = createRepository(null, srcPath, null, pegRevision, revision, null);
long revisionNumber = getRevisionNumber(revision, repository, srcPath);
exportedRevision = doRemoteExport(repository, revisionNumber, dstPath, eolStyle, overwrite, depth);
} else {
if (revision == SVNRevision.UNDEFINED) {
revision = SVNRevision.WORKING;
}
copyVersionedDir(srcPath, dstPath, revision, eolStyle, overwrite, depth);
}
dispatchEvent(SVNEventFactory.createSVNEvent(null, SVNNodeKind.NONE, null, exportedRevision, SVNEventAction.UPDATE_COMPLETED, null, null, null));
return exportedRevision;
}
/**
* Substitutes the beginning part of a Working Copy's URL with a new one.
*
* <p>
* When a repository root location or a URL schema is changed the old URL of the
* Working Copy which starts with <code>oldURL</code> should be substituted for a
* new URL beginning - <code>newURL</code>.
*
* @param dst a Working Copy item's path
* @param oldURL the old beginning part of the repository's URL that should
* be overwritten
* @param newURL a new beginning part for the repository location that
* will overwrite <code>oldURL</code>
* @param recursive if <span class="javakeyword">true</span> and <code>dst</code> is
* a directory then the entire tree will be relocated, otherwise if
* <span class="javakeyword">false</span> - only <code>dst</code> itself
* @throws SVNException
*/
public void doRelocate(File dst, SVNURL oldURL, SVNURL newURL, boolean recursive) throws SVNException {
SVNWCAccess wcAccess = createWCAccess();
try {
SVNAdminArea adminArea = wcAccess.probeOpen(dst, true, recursive ? SVNWCAccess.INFINITE_DEPTH : 0);
String name = dst.equals(adminArea.getRoot()) ? adminArea.getThisDirName() : dst.getName();
String from = oldURL.toString();
String to = newURL.toString();
if (from.endsWith("/")) {
from = from.substring(0, from.length() - 1);
}
if (to.endsWith("/")) {
to = to.substring(0, to.length() - 1);
}
doRelocate(adminArea, name, from, to, recursive, new SVNHashMap());
} finally {
wcAccess.close();
}
}
/**
* Canonicalizes all urls in the specified Working Copy.
*
* @param dst a WC path
* @param omitDefaultPort if <span class="javakeyword">true</span> then removes all
* port numbers from urls which equal to default ones, otherwise
* does not
* @param recursive recurses an operation
* @throws SVNException
*/
public void doCanonicalizeURLs(File dst, boolean omitDefaultPort, boolean recursive) throws SVNException {
SVNWCAccess wcAccess = createWCAccess();
try {
SVNAdminAreaInfo adminAreaInfo = wcAccess.openAnchor(dst, true, recursive ? SVNWCAccess.INFINITE_DEPTH : 0);
SVNAdminArea target = adminAreaInfo.getTarget();
SVNEntry entry = wcAccess.getEntry(dst, false);
String name = target.getThisDirName();
if (entry != null && entry.isFile()) {
name = entry.getName();
}
doCanonicalizeURLs(adminAreaInfo, target, name, omitDefaultPort, recursive);
if (recursive && !isIgnoreExternals()) {
for(Iterator externals = adminAreaInfo.getNewExternals().keySet().iterator(); externals.hasNext();) {
String path = (String) externals.next();
String external = (String) adminAreaInfo.getNewExternals().get(path);
SVNExternal[] exts = SVNExternal.parseExternals(path, external);
File owner = new File(adminAreaInfo.getAnchor().getRoot(), path);
for (int i = 0; i < exts.length; i++) {
File externalFile = new File(owner, exts[i].getPath());
try {
doCanonicalizeURLs(externalFile, omitDefaultPort, true);
} catch (SVNCancelException e) {
throw e;
} catch (SVNException e) {
getDebugLog().logFine(SVNLogType.WC, e);
}
}
}
}
} finally {
wcAccess.close();
}
}
public void setExportExpandsKeywords(boolean expand) {
myIsExportExpandsKeywords = expand;
}
public boolean isExportExpandsKeywords() {
return myIsExportExpandsKeywords;
}
private void copyVersionedDir(File from, File to, SVNRevision revision, String eolStyle, boolean force, SVNDepth depth) throws SVNException {
SVNWCAccess wcAccess = createWCAccess();
SVNAdminArea adminArea = wcAccess.probeOpen(from, false, 0);
SVNEntry entry = null;
try {
entry = wcAccess.getVersionedEntry(from, false);
} catch (SVNException svne) {
wcAccess.close();
throw svne;
}
if (revision == SVNRevision.WORKING && entry.isScheduledForDeletion()) {
return;
}
if (revision != SVNRevision.WORKING && entry.isScheduledForAddition()) {
return;
}
if (entry.isDirectory()) {
// create dir
boolean dirCreated = to.mkdirs();
if (!to.exists() || to.isFile()) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot create directory ''{0}''", to);
SVNErrorManager.error(err, SVNLogType.WC);
}
if (!dirCreated && to.isDirectory() && !force) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE, "''{0}'' already exists and will not be owerwritten unless forced", to);
SVNErrorManager.error(err, SVNLogType.WC);
}
// read entries
for (Iterator ents = adminArea.entries(false); ents.hasNext();) {
SVNEntry childEntry = (SVNEntry) ents.next();
if (childEntry.isDirectory()) {
if (adminArea.getThisDirName().equals(childEntry.getName())) {
continue;
} else if (depth == SVNDepth.INFINITY) {
File childTo = new File(to, childEntry.getName());
File childFrom = new File(from, childEntry.getName());
copyVersionedDir(childFrom, childTo, revision, eolStyle, force, depth);
}
} else if (childEntry.isFile()) {
File childTo = new File(to, childEntry.getName());
copyVersionedFile(childTo, adminArea, childEntry.getName(), revision, eolStyle);
}
}
if (!isIgnoreExternals() && depth == SVNDepth.INFINITY && entry.getDepth() == SVNDepth.INFINITY) {
SVNVersionedProperties properties = adminArea.getProperties(adminArea.getThisDirName());
String externalsValue = properties.getStringPropertyValue(SVNProperty.EXTERNALS);
if (externalsValue != null) {
SVNExternal[] externals = SVNExternal.parseExternals(adminArea.getRoot().getAbsolutePath(), externalsValue);
for (int i = 0; i < externals.length; i++) {
SVNExternal info = externals[i];
File srcPath = new File(adminArea.getRoot(), info.getPath());
File dstPath = new File(to, info.getPath());
if (SVNPathUtil.getSegmentsCount(info.getPath()) > 1) {
if (!dstPath.getParentFile().exists() && !dstPath.getParentFile().mkdirs()) {
SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.CLIENT_IS_DIRECTORY, "Could not create directory ''{0}''", dstPath.getParentFile()), SVNLogType.WC);
}
}
copyVersionedDir(srcPath, dstPath, revision, eolStyle, force, SVNDepth.INFINITY);
}
}
}
} else if (entry.isFile()) {
copyVersionedFile(to, adminArea, entry.getName(), revision, eolStyle);
}
wcAccess.close();
}
private void copyVersionedFile(File dstPath, SVNAdminArea adminArea, String fileName, SVNRevision revision, String eol) throws SVNException {
SVNEntry entry = adminArea.getEntry(fileName, false);
if (revision == SVNRevision.WORKING && entry.isScheduledForDeletion()) {
return;
}
if (revision != SVNRevision.WORKING && entry.isScheduledForAddition()) {
return;
}
boolean modified = false;
SVNVersionedProperties props = null;
long timestamp;
if (revision != SVNRevision.WORKING) {
props = adminArea.getBaseProperties(fileName);
} else {
props = adminArea.getProperties(fileName);
modified = adminArea.hasTextModifications(fileName, false);
}
boolean special = props.getPropertyValue(SVNProperty.SPECIAL) != null;
boolean executable = props.getPropertyValue(SVNProperty.EXECUTABLE) != null;
String keywords = props.getStringPropertyValue(SVNProperty.KEYWORDS);
String charsetProp = props.getStringPropertyValue(SVNProperty.CHARSET);
String charset = SVNTranslator.getCharset(charsetProp, adminArea.getFile(fileName).getPath(), getOptions());
byte[] eols = eol != null ? SVNTranslator.getEOL(eol, getOptions()) : null;
if (eols == null) {
eol = props.getStringPropertyValue(SVNProperty.EOL_STYLE);
eols = SVNTranslator.getEOL(eol, getOptions());
}
if (modified && !special) {
timestamp = adminArea.getFile(fileName).lastModified();
} else {
timestamp = SVNDate.parseDateAsMilliseconds(entry.getCommittedDate());
}
Map keywordsMap = null;
if (keywords != null) {
String rev = Long.toString(entry.getCommittedRevision());
String author;
if (modified) {
author = "(local)";
rev += "M";
} else {
author = entry.getAuthor();
}
keywordsMap = SVNTranslator.computeKeywords(keywords, entry.getURL(), author, entry.getCommittedDate(), rev, getOptions());
}
File srcFile = revision == SVNRevision.WORKING ? adminArea.getFile(fileName) : adminArea.getBaseFile(fileName, false);
SVNFileType fileType = SVNFileType.getType(srcFile);
if (fileType == SVNFileType.SYMLINK && revision == SVNRevision.WORKING) {
// base will be translated OK, but working not.
File tmpBaseFile = adminArea.getBaseFile(fileName, true);
try {
SVNTranslator.translate(srcFile, tmpBaseFile, charset, eols, keywordsMap, special, false);
SVNTranslator.translate(tmpBaseFile, dstPath, charset, eols, keywordsMap, special, true);
} finally {
tmpBaseFile.delete();
}
} else {
SVNTranslator.translate(srcFile, dstPath, charset, eols, keywordsMap, special, true);
}
if (executable) {
SVNFileUtil.setExecutable(dstPath, true);
}
if (!special && timestamp > 0) {
dstPath.setLastModified(timestamp);
}
}
private long doRemoteExport(SVNRepository repository, final long revNumber, File dstPath, String eolStyle, boolean force, SVNDepth depth) throws SVNException {
SVNNodeKind dstKind = repository.checkPath("", revNumber);
if (dstKind == SVNNodeKind.DIR) {
SVNExportEditor editor = new SVNExportEditor(this, repository.getLocation().toString(), dstPath, force, eolStyle, isExportExpandsKeywords(), getOptions());
repository.update(revNumber, null, depth, false, new ISVNReporterBaton() {
public void report(ISVNReporter reporter) throws SVNException {
reporter.setPath("", null, revNumber, SVNDepth.INFINITY, true);
reporter.finishReport();
}
}, SVNCancellableEditor.newInstance(editor, this, getDebugLog()));
// nothing may be created.
SVNFileType fileType = SVNFileType.getType(dstPath);
if (fileType == SVNFileType.NONE) {
editor.openRoot(revNumber);
}
if (!isIgnoreExternals() && depth == SVNDepth.INFINITY) {
Map externals = editor.getCollectedExternals();
handleExternals(dstPath, Collections.EMPTY_MAP, externals, Collections.EMPTY_MAP, repository.getLocation(), repository.getRepositoryRoot(true),
depth, true, true);
}
} else if (dstKind == SVNNodeKind.FILE) {
String url = repository.getLocation().toString();
if (dstPath.isDirectory()) {
dstPath = new File(dstPath, SVNEncodingUtil.uriDecode(SVNPathUtil.tail(url)));
}
if (dstPath.exists()) {
if (!force) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE, "Path ''{0}'' already exists", dstPath);
SVNErrorManager.error(err, SVNLogType.WC);
}
} else {
dstPath.getParentFile().mkdirs();
}
SVNProperties properties = new SVNProperties();
OutputStream os = null;
File tmpFile = SVNFileUtil.createUniqueFile(dstPath.getParentFile(), ".export", ".tmp", false);
try {
os = SVNFileUtil.openFileForWriting(tmpFile);
try {
repository.getFile("", revNumber, properties, new SVNCancellableOutputStream(os, this));
} finally {
SVNFileUtil.closeFile(os);
}
if (force && dstPath.exists()) {
SVNFileUtil.deleteAll(dstPath, this);
}
if (!isExportExpandsKeywords()) {
properties.put(SVNProperty.MIME_TYPE, "application/octet-stream");
}
boolean binary = SVNProperty.isBinaryMimeType(properties.getStringValue(SVNProperty.MIME_TYPE));
String charset = SVNTranslator.getCharset(properties.getStringValue(SVNProperty.CHARSET), url, getOptions());
Map keywords = SVNTranslator.computeKeywords(properties.getStringValue(SVNProperty.KEYWORDS), url,
properties.getStringValue(SVNProperty.LAST_AUTHOR),
properties.getStringValue(SVNProperty.COMMITTED_DATE),
properties.getStringValue(SVNProperty.COMMITTED_REVISION), getOptions());
byte[] eols = null;
if (SVNProperty.EOL_STYLE_NATIVE.equals(properties.getStringValue(SVNProperty.EOL_STYLE))) {
eols = SVNTranslator.getEOL(eolStyle != null ? eolStyle : properties.getStringValue(SVNProperty.EOL_STYLE), getOptions());
} else if (properties.containsName(SVNProperty.EOL_STYLE)) {
eols = SVNTranslator.getEOL(properties.getStringValue(SVNProperty.EOL_STYLE), getOptions());
}
if (binary) {
charset = null;
eols = null;
keywords = null;
}
SVNTranslator.translate(tmpFile, dstPath, charset, eols, keywords, properties.getStringValue(SVNProperty.SPECIAL) != null, true);
} finally {
SVNFileUtil.deleteFile(tmpFile);
}
if (properties.getStringValue(SVNProperty.EXECUTABLE) != null) {
SVNFileUtil.setExecutable(dstPath, true);
}
dispatchEvent(SVNEventFactory.createSVNEvent(dstPath, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPDATE_ADD, null, null, null));
} else {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_ILLEGAL_URL, "URL ''{0}'' doesn't exist", repository.getLocation());
SVNErrorManager.error(err, SVNLogType.WC);
}
return revNumber;
}
private void doCanonicalizeURLs(SVNAdminAreaInfo adminAreaInfo, SVNAdminArea adminArea, String name, boolean omitDefaultPort, boolean recursive) throws SVNException {
boolean save = false;
checkCancelled();
if (!adminArea.getThisDirName().equals(name)) {
SVNEntry entry = adminArea.getEntry(name, true);
save = canonicalizeEntry(entry, omitDefaultPort);
adminArea.getWCProperties(name).setPropertyValue(SVNProperty.WC_URL, null);
if (save) {
adminArea.saveEntries(false);
}
return;
}
if (!isIgnoreExternals()) {
SVNPropertyValue externalsValue = adminArea.getProperties(adminArea.getThisDirName()).getPropertyValue(SVNProperty.EXTERNALS);
if (externalsValue != null) {
String ownerPath = adminArea.getRelativePath(adminAreaInfo.getAnchor());
String externals = externalsValue == null ? null : externalsValue.getString();
adminAreaInfo.addExternal(ownerPath, externals, externals);
if (externalsValue != null) {
externalsValue = SVNPropertyValue.create(canonicalizeExtenrals(externals, omitDefaultPort));
adminArea.getProperties(adminArea.getThisDirName()).setPropertyValue(SVNProperty.EXTERNALS, externalsValue);
}
}
}
SVNEntry rootEntry = adminArea.getEntry(adminArea.getThisDirName(), true);
save = canonicalizeEntry(rootEntry, omitDefaultPort);
adminArea.getWCProperties(adminArea.getThisDirName()).setPropertyValue(SVNProperty.WC_URL, null);
// now all child entries that doesn't has repos/url has new values.
for(Iterator ents = adminArea.entries(true); ents.hasNext();) {
SVNEntry entry = (SVNEntry) ents.next();
if (adminArea.getThisDirName().equals(entry.getName())) {
continue;
}
checkCancelled();
if (recursive && entry.isDirectory() &&
(entry.isScheduledForAddition() || !entry.isDeleted()) &&
!entry.isAbsent()) {
SVNAdminArea childArea = adminArea.getWCAccess().retrieve(adminArea.getFile(entry.getName()));
if (childArea != null) {
doCanonicalizeURLs(adminAreaInfo, childArea, "", omitDefaultPort, recursive);
}
}
save |= canonicalizeEntry(entry, omitDefaultPort);
SVNVersionedProperties properties = adminArea.getWCProperties(entry.getName());
if (properties != null) {
properties.setPropertyValue(SVNProperty.WC_URL, null);
}
}
if (save) {
adminArea.saveEntries(true);
}
}
private static String canonicalizeExtenrals(String externals, boolean omitDefaultPort) throws SVNException {
if (externals == null) {
return null;
}
StringBuffer canonicalized = new StringBuffer();
for(StringTokenizer lines = new StringTokenizer(externals, "\r\n", true); lines.hasMoreTokens();) {
String line = lines.nextToken();
if (line.trim().length() == 0 || line.trim().startsWith("#")
|| line.indexOf('\r') >= 0 || line.indexOf('\n') >= 0) {
canonicalized.append(line);
continue;
}
String[] tokens = line.split("[ \t]");
int index = tokens.length - 1;
SVNURL url = null;
if (index >= 1) {
try {
url = SVNURL.parseURIEncoded(tokens[index]);
} catch (SVNException e) {
url = null;
}
}
SVNURL canonicalURL = canonicalizeURL(url, omitDefaultPort);
if (canonicalURL == null) {
canonicalized.append(line);
} else {
canonicalized.append(tokens[0]);
canonicalized.append(' ');
if (index == 2) {
canonicalized.append(tokens[1]);
canonicalized.append(' ');
}
canonicalized.append(canonicalURL.toString());
}
}
return canonicalized.toString();
}
private static boolean canonicalizeEntry(SVNEntry entry, boolean omitDefaultPort) throws SVNException {
boolean updated = false;
SVNURL root = canonicalizeURL(entry.getRepositoryRootURL(), omitDefaultPort);
if (root != null) {
updated |= entry.setRepositoryRootURL(root);
}
SVNURL url = canonicalizeURL(entry.getSVNURL(), omitDefaultPort);
if (url != null) {
updated |= entry.setURL(url.toString());
}
SVNURL copyFrom = canonicalizeURL(entry.getCopyFromSVNURL(), omitDefaultPort);
if (copyFrom != null) {
updated |= entry.setCopyFromURL(copyFrom.toString());
}
return updated;
}
private static SVNURL canonicalizeURL(SVNURL url, boolean omitDefaultPort) throws SVNException {
if (url == null || url.getPort() <= 0) {
// no url or file url.
return null;
}
int defaultPort = SVNURL.getDefaultPortNumber(url.getProtocol());
if (defaultPort <= 0) {
// file or svn+ext URL.
return null;
}
if (omitDefaultPort) {
// remove port if it is same as default.
if (url.hasPort() && url.getPort() == defaultPort) {
return SVNURL.create(url.getProtocol(), url.getUserInfo(), url.getHost(), -1, url.getPath(), false);
}
} else if (!url.hasPort()) {
// set port if there is no port set.
return SVNURL.create(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), false);
}
return null;
}
private void handleExternals(File root, Map oldExternals, Map newExternals, Map depths, SVNURL fromURL, SVNURL rootURL, SVNDepth requestedDepth, boolean isExport, boolean updateUnchanged) throws SVNException {
Set diff = new SVNHashSet();
if (oldExternals != null) {
diff.addAll(oldExternals.keySet());
}
if (newExternals != null) {
diff.addAll(newExternals.keySet());
}
// now we have diff.
for (Iterator diffPaths = diff.iterator(); diffPaths.hasNext();) {
String diffPath = (String) diffPaths.next();
SVNDepth ambientDepth = depths == Collections.EMPTY_MAP ? SVNDepth.INFINITY : (SVNDepth) depths.get(diffPath);
if (ambientDepth == null) {
// TODO convert diffpath to full path.
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Traversal of ''{0}'' found no ambient depth", diffPath);
SVNErrorManager.error(err, SVNLogType.WC);
}
if (!ambientDepth.isRecursive() || !requestedDepth.isRecursive()) {
// skip externals - either folder depth is not recursive,
// or requested depth is not recursive.
continue;
}
String oldValue = (String) oldExternals.get(diffPath);
String newValue = (String) newExternals.get(diffPath);
// TODO convert diffpath to full path.
SVNExternal[] previous = oldValue != null ? SVNExternal.parseExternals(diffPath, oldValue) : null;
SVNExternal[] current = newValue != null ? SVNExternal.parseExternals(diffPath, newValue) : null;
Map oldParsedExternals = new LinkedHashMap();
Map newParsedExternals = new LinkedHashMap();
// put to another hashes.
for (int i = 0; current != null && i < current.length; i++) {
newParsedExternals.put(current[i].getPath(), current[i]);
}
for (int i = 0; previous != null && i < previous.length; i++) {
oldParsedExternals.put(previous[i].getPath(), previous[i]);
}
// finally handle changes.
ExternalDiff externalDiff = new ExternalDiff();
externalDiff.isExport = isExport;
externalDiff.isUpdateUnchanged = updateUnchanged;
externalDiff.rootURL = rootURL;
for (Iterator paths = oldParsedExternals.keySet().iterator(); paths.hasNext();) {
String path = (String) paths.next();
externalDiff.oldExternal = (SVNExternal) oldParsedExternals.get(path);
externalDiff.newExternal = (SVNExternal) newParsedExternals.get(path);
externalDiff.owner = new File(root, diffPath);
externalDiff.ownerURL = fromURL.appendPath(diffPath, false);
handleExternalChange(externalDiff.oldExternal.getPath(), externalDiff);
}
for (Iterator paths = newParsedExternals.keySet().iterator(); paths.hasNext();) {
String path = (String) paths.next();
if (!oldParsedExternals.containsKey(path)) {
externalDiff.oldExternal = null;
externalDiff.newExternal = (SVNExternal) newParsedExternals.get(path);
externalDiff.owner = new File(root, diffPath);
externalDiff.ownerURL = fromURL.appendPath(diffPath, false);
handleExternalChange(externalDiff.newExternal.getPath(), externalDiff);
}
}
}
}
private void handleExternalChange(String targetDir, ExternalDiff externalDiff) throws SVNException {
File target = new File(externalDiff.owner, targetDir);
SVNURL oldURL = null;
SVNURL newURL = null;
String externalDefinition = null;
if (externalDiff.oldExternal != null && !externalDiff.isExport) {
oldURL = externalDiff.oldExternal.resolveURL(externalDiff.rootURL, externalDiff.ownerURL);
externalDefinition = externalDiff.oldExternal.getRawValue();
}
SVNRevision externalRevision = SVNRevision.UNDEFINED;
SVNRevision externalPegRevision = SVNRevision.UNDEFINED;
if (externalDiff.newExternal != null) {
newURL = externalDiff.newExternal.resolveURL(externalDiff.rootURL, externalDiff.ownerURL);
externalRevision = externalDiff.newExternal.getRevision();
externalPegRevision = externalDiff.newExternal.getPegRevision();
externalDefinition = externalDiff.newExternal.getRawValue();
}
if (oldURL == null && newURL == null) {
return;
}
SVNRevision[] revs = getExternalsHandler().handleExternal(target, newURL, externalRevision,
externalPegRevision, externalDefinition, SVNRevision.UNDEFINED);
if (revs == null) {
SVNEvent event = SVNEventFactory.createSVNEvent(target, SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.SKIP, SVNEventAction.UPDATE_EXTERNAL, null, null);
dispatchEvent(event);
return;
}
externalRevision = revs.length > 0 && revs[0] != null ? revs[0] : externalRevision;
externalPegRevision = revs.length > 1 && revs[1] != null ? revs[1] : externalPegRevision;
try {
setEventPathPrefix("path");
if (oldURL == null) {
target.mkdirs();
dispatchEvent(SVNEventFactory.createSVNEvent(target, SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPDATE_EXTERNAL, null, null, null));
if (externalDiff.isExport) {
doExport(newURL, target, externalPegRevision, externalRevision, null, true, SVNDepth.INFINITY);
} else {
doCheckout(newURL, target, externalPegRevision, externalRevision, SVNDepth.INFINITY, false);
}
} else if (newURL == null) {
SVNWCAccess wcAccess = createWCAccess();
SVNAdminArea area = wcAccess.open(target, true, SVNWCAccess.INFINITE_DEPTH);
SVNException error = null;
try {
area.removeFromRevisionControl(area.getThisDirName(), true, false);
} catch (SVNException svne) {
error = svne;
}
if (error == null || error.getErrorMessage().getErrorCode() == SVNErrorCode.WC_LEFT_LOCAL_MOD) {
try {
wcAccess.close();
} catch (SVNException svne) {
error = error == null ? svne : error;
}
}
if (error != null && error.getErrorMessage().getErrorCode() != SVNErrorCode.WC_LEFT_LOCAL_MOD) {
throw error;
}
} else if (externalDiff.isUpdateUnchanged || !externalDiff.compareExternals(oldURL, newURL)) {
SVNFileType fileType = SVNFileType.getType(target);
boolean empty = false;
if (fileType == SVNFileType.DIRECTORY) {
File[] children = target.listFiles();
if (children != null && children.length == 0) {
empty = true;
}
}
if (fileType == SVNFileType.DIRECTORY && !empty) {
dispatchEvent(SVNEventFactory.createSVNEvent(target, SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPDATE_EXTERNAL, null, null, null));
SVNWCAccess wcAccess = createWCAccess();
SVNAdminArea area = wcAccess.open(target, true, 0);
SVNEntry entry = area.getEntry(area.getThisDirName(), false);
wcAccess.close();
String url = entry.getURL();
if (entry != null && entry.getURL() != null) {
if (newURL.toString().equals(url)) {
doUpdate(target, externalRevision, SVNDepth.UNKNOWN, true, false);
return;
} else if (entry.getRepositoryRoot() != null) {
if (!SVNPathUtil.isAncestor(entry.getRepositoryRoot(), newURL.toString())) {
SVNRepository repos = createRepository(newURL, null, null, true);
SVNURL reposRoot = repos.getRepositoryRoot(true);
try {
doRelocate(target, entry.getSVNURL(), reposRoot, true);
} catch (SVNException svne) {
if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.WC_INVALID_RELOCATION ||
svne.getErrorMessage().getErrorCode() == SVNErrorCode.CLIENT_INVALID_RELOCATION) {
deleteExternal(target);
target.mkdirs();
doCheckout(newURL, target, externalPegRevision, externalRevision, SVNDepth.INFINITY, false);
return;
}
throw svne;
}
}
doSwitch(target, newURL, externalPegRevision,
externalRevision, SVNDepth.INFINITY, false, true);
return;
}
}
deleteExternal(target);
target.mkdirs();
dispatchEvent(SVNEventFactory.createSVNEvent(target, SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPDATE_EXTERNAL, null, null, null));
doCheckout(newURL, target, externalPegRevision, externalRevision, SVNDepth.INFINITY, false);
return;
}
if (fileType != SVNFileType.DIRECTORY) {
target.mkdirs();
}
dispatchEvent(SVNEventFactory.createSVNEvent(target, SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPDATE_EXTERNAL, null, null, null));
doCheckout(newURL, target, externalPegRevision, externalRevision, SVNDepth.INFINITY, true);
}
} catch (SVNCancelException cancel) {
throw cancel;
} catch (SVNException e) {
SVNDebugLog.getDefaultLog().logFine(SVNLogType.WC, e);
SVNEvent event = SVNEventFactory.createSVNEvent(target, SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.SKIP, SVNEventAction.UPDATE_EXTERNAL, e.getErrorMessage(), null);
dispatchEvent(event);
} finally {
setEventPathPrefix(null);
}
}
private void deleteExternal(File external) throws SVNException {
SVNWCAccess wcAccess = createWCAccess();
SVNAdminArea adminArea = wcAccess.open(external, true, SVNWCAccess.INFINITE_DEPTH);
SVNException error = null;
try {
adminArea.removeFromRevisionControl(adminArea.getThisDirName(), true, false);
} catch (SVNException svne) {
getDebugLog().logFine(SVNLogType.WC, svne);
error = svne;
}
if (error == null || error.getErrorMessage().getErrorCode() == SVNErrorCode.WC_LEFT_LOCAL_MOD) {
wcAccess.close();
}
if (error != null && error.getErrorMessage().getErrorCode() == SVNErrorCode.WC_LEFT_LOCAL_MOD) {
external.getParentFile().mkdirs();
File newLocation = SVNFileUtil.createUniqueFile(external.getParentFile(), external.getName(), ".OLD", false);
SVNFileUtil.rename(external, newLocation);
} else if (error != null) {
throw error;
}
}
private Map validateRelocateTargetURL(SVNURL targetURL, String expectedUUID, Map validatedURLs, boolean isRoot) throws SVNException {
if (validatedURLs == null) {
return null;
}
for(Iterator targetURLs = validatedURLs.keySet().iterator(); targetURLs.hasNext();) {
SVNURL validatedURL = (SVNURL) targetURLs.next();
if (targetURL.toString().startsWith(validatedURL.toString())) {
if (isRoot && !targetURL.equals(validatedURL)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_RELOCATION, "''{0}'' is not the root of the repository", targetURL);
SVNErrorManager.error(err, SVNLogType.WC);
}
String validatedUUID = (String) validatedURLs.get(validatedURL);
if (expectedUUID != null && !expectedUUID.equals(validatedUUID)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_RELOCATION, "The repository at ''{0}'' has uuid ''{1}'', but the WC has ''{2}''",
new Object[] {validatedURL, expectedUUID, validatedUUID});
SVNErrorManager.error(err, SVNLogType.WC);
}
return validatedURLs;
}
}
SVNRepository repos = createRepository(targetURL, null, null, false);
try {
SVNURL actualRoot = repos.getRepositoryRoot(true);
if (isRoot && !targetURL.equals(actualRoot)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_RELOCATION, "''{0}'' is not the root of the repository", targetURL);
SVNErrorManager.error(err, SVNLogType.WC);
}
String actualUUID = repos.getRepositoryUUID(true);
if (expectedUUID != null && !expectedUUID.equals(actualUUID)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_RELOCATION, "The repository at ''{0}'' has uuid ''{1}'', but the WC has ''{2}''",
new Object[] {targetURL, expectedUUID, actualUUID});
SVNErrorManager.error(err, SVNLogType.WC);
}
validatedURLs.put(targetURL, actualUUID);
} finally {
repos.closeSession();
}
return validatedURLs;
}
private Map relocateEntry(SVNEntry entry, String from, String to, Map validatedURLs) throws SVNException {
if (entry.getRepositoryRoot() != null) {
// that is what i do not understand :)
String repos = entry.getRepositoryRoot();
if (from.length() > repos.length()) {
String fromPath = from.substring(repos.length());
if (!to.endsWith(fromPath)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_RELOCATION, "Relocate can only change the repository part of an URL");
SVNErrorManager.error(err, SVNLogType.WC);
}
from = repos;
to = to.substring(0, to.length() - fromPath.length());
}
if (repos.startsWith(from)) {
entry.setRepositoryRoot(to + repos.substring(from.length()));
validatedURLs = validateRelocateTargetURL(entry.getRepositoryRootURL(), entry.getUUID(), validatedURLs, true);
}
}
if (entry.getURL() != null && entry.getURL().startsWith(from)) {
entry.setURL(to + entry.getURL().substring(from.length()));
if (entry.getUUID() != null && validatedURLs != null) {
validatedURLs = validateRelocateTargetURL(entry.getSVNURL(), entry.getUUID(), validatedURLs, false);
}
}
if (entry.getCopyFromURL() != null && entry.getCopyFromURL().startsWith(from)) {
entry.setCopyFromURL(to + entry.getCopyFromURL().substring(from.length()));
if (entry.getUUID() != null && validatedURLs != null) {
validatedURLs = validateRelocateTargetURL(entry.getCopyFromSVNURL(), entry.getUUID(), validatedURLs, false);
}
}
return validatedURLs;
}
private Map doRelocate(SVNAdminArea adminArea, String name, String from, String to, boolean recursive, Map validatedURLs) throws SVNException {
SVNEntry entry = adminArea.getEntry(name, true);
if (entry == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND);
SVNErrorManager.error(err, SVNLogType.WC);
}
if (entry.isFile()) {
relocateEntry(entry, from, to, validatedURLs);
SVNPropertiesManager.deleteWCProperties(adminArea, name, false);
adminArea.saveEntries(false);
return validatedURLs;
}
validatedURLs = relocateEntry(entry, from, to, validatedURLs);
SVNWCAccess wcAccess = adminArea.getWCAccess();
for (Iterator entries = adminArea.entries(true); entries.hasNext();) {
SVNEntry childEntry = (SVNEntry) entries.next();
if (adminArea.getThisDirName().equals(childEntry.getName())) {
continue;
}
if (recursive && childEntry.isDirectory() &&
(childEntry.isScheduledForAddition() || !childEntry.isDeleted()) && !childEntry.isAbsent()) {
File childDir = adminArea.getFile(childEntry.getName());
if (wcAccess.isMissing(childDir)) {
continue;
}
SVNAdminArea childArea = wcAccess.retrieve(childDir);
validatedURLs = doRelocate(childArea, childArea.getThisDirName(), from, to, recursive, validatedURLs);
}
validatedURLs = relocateEntry(childEntry, from, to, validatedURLs);
SVNPropertiesManager.deleteWCProperties(adminArea, childEntry.getName(), false);
}
SVNPropertiesManager.deleteWCProperties(adminArea, "", false);
adminArea.saveEntries(false);
return validatedURLs;
}
private static class ExternalDiff {
public SVNExternal oldExternal;
public SVNExternal newExternal;
public File owner;
public SVNURL ownerURL;
public SVNURL rootURL;
public boolean isExport;
public boolean isUpdateUnchanged;
public boolean compareExternals(SVNURL oldURL, SVNURL newURL) {
return oldURL.equals(newURL) &&
oldExternal.getRevision().equals(newExternal.getRevision()) &&
oldExternal.getPegRevision().equals(newExternal.getPegRevision());
}
}
}