package org.tmatesoft.svn.core.internal.wc16; import org.tmatesoft.svn.core.*; import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.internal.util.*; import org.tmatesoft.svn.core.internal.wc.*; import org.tmatesoft.svn.core.internal.wc.admin.*; import org.tmatesoft.svn.core.io.*; import org.tmatesoft.svn.core.wc.*; import org.tmatesoft.svn.util.SVNDebugLog; import org.tmatesoft.svn.util.SVNLogType; import java.io.File; import java.io.OutputStream; import java.util.*; /** * 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.3 * @author TMate Software Ltd. * @since 1.2 * @see <a target="_top" href="http://svnkit.com/kb/examples/">Examples</a> */ public class SVNUpdateClient16 extends SVNBasicDelegate { 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 SVNUpdateClient16(ISVNAuthenticationManager authManager, ISVNOptions options) { super(authManager, options); setExportExpandsKeywords(true); setExternalsHandler(ISVNExternalsHandler.DEFAULT); } /** * 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 SVNUpdateClient16(ISVNRepositoryPool repositoryPool, ISVNOptions options) { super(repositoryPool, options); setExportExpandsKeywords(true); setExternalsHandler(ISVNExternalsHandler.DEFAULT); } /** * 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) { if (externalsHandler == null) { externalsHandler = ISVNExternalsHandler.DEFAULT; } this.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() { return this.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 tolerating 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 doSwitchImpl(SVNWCAccess wcAccess, File path, SVNURL url, SVNRevision pegRevision, SVNRevision revision, SVNDepth depth, boolean allowUnversionedObstructions, boolean depthIsSticky) throws SVNException { if (!SVNWCUtil.isVersionedDirectory(path.isDirectory() ? path : path.getParentFile())) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "''{0}'' does not appear to be a working copy path", path); SVNErrorManager.error(err, SVNLogType.CLIENT); } if (depth == SVNDepth.UNKNOWN) { depthIsSticky = false; } if (depthIsSticky && depth == SVNDepth.EXCLUDE) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Cannot both exclude and switch a path"); SVNErrorManager.error(err, SVNLogType.WC); } boolean closeAccess = wcAccess == null; try { SVNAdminAreaInfo info = null; if (wcAccess != null) { SVNWCAccess tmpAccess = null; try { tmpAccess = createWCAccess(); info = tmpAccess.openAnchor(path, false, SVNWCAccess.INFINITE_DEPTH); } finally { tmpAccess.close(); } SVNAdminArea anchor = info.getAnchor(); SVNAdminArea target = info.getTarget(); anchor = wcAccess.retrieve(anchor.getRoot()); target = wcAccess.retrieve(target.getRoot()); info.setAnchor(anchor); info.setTarget(target); info.setWCAccess(wcAccess); } else { wcAccess = createWCAccess(); info = wcAccess.openAnchor(path, true, SVNWCAccess.INFINITE_DEPTH); } final SVNReporter reporter = new SVNReporter(info, path, true, false, depth, false, false, !depthIsSticky, 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]; SVNRepository repository = createRepository(url, null, anchorArea, pegRevision, revision, revs); long revNumber = revs[0]; url = repository.getLocation(); 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[] { sourceURL.toString(), sourceRoot.toString() }); SVNErrorManager.error(err, SVNLogType.WC); } if (depthIsSticky && depth.compareTo(SVNDepth.INFINITY) < 0) { SVNEntry targetEntry = anchorArea.getEntry(info.getTargetName(), true); if (targetEntry != null && targetEntry.isDirectory()) { SVNWCManager.crop(info, depth); } } repository.setLocation(sourceURL, false); String[] preservedExts = getOptions().getPreservedConflictFileExtensions(); ISVNUpdateEditor editor = wcAccess.createUpdateEditor(info, url.toString(), allowUnversionedObstructions, depthIsSticky, depth, preservedExts, null, false); ISVNEditor filterEditor = SVNAmbientDepthFilterEditor.wrap(editor, info, 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()) { url = target == null ? url : url.removePathTail(); handleExternals(wcAccess, info.getAnchor().getRoot(), info.getOldExternals(), info.getNewExternals(), info.getDepths(), url, sourceRoot, depth, false, true); } dispatchEvent(SVNEventFactory.createSVNEvent(info.getTarget().getRoot(), SVNNodeKind.NONE, null, targetRevision, SVNEventAction.UPDATE_COMPLETED, null, null, null, reporter .getReportedFilesCount(), reporter.getTotalFilesCount())); return targetRevision; } finally { if (closeAccess) { wcAccess.close(); } sleepForTimeStamp(); } } 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 = depthIsSticky ? -1 : 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); } if (depthIsSticky && depth.compareTo(SVNDepth.INFINITY) < 0) { SVNEntry targetEntry = anchorArea.getEntry(adminInfo.getTargetName(), true); if (targetEntry != null && targetEntry.isDirectory()) { SVNWCManager.crop(adminInfo, depth); if (depth == SVNDepth.EXCLUDE) { return -1; } } } 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, !depthIsSticky, getDebugLog()); String target = "".equals(adminInfo.getTargetName()) ? null : adminInfo.getTargetName(); long revNumber = getRevisionNumber(revision, repos, path); final SVNURL reposRoot = repos.getRepositoryRoot(true); wcAccess.setRepositoryRoot(path, reposRoot); final SVNRepository[] repos2 = new SVNRepository[1]; ISVNFileFetcher fileFetcher = new ISVNFileFetcher() { public long fetchFile(String path, long revision, OutputStream os, SVNProperties properties) throws SVNException { SVNURL url = reposRoot.appendPath(SVNPathUtil.removeTail(path), false); if (repos2[0] == null) { repos2[0] = createRepository(url, null, null, false); } else { repos2[0].setLocation(url, false); } return repos2[0].getFile(SVNPathUtil.tail(path), revision, properties, os); } }; ISVNUpdateEditor editor = wcAccess.createUpdateEditor(adminInfo, null, allowUnversionedObstructions, depthIsSticky, depth, preservedExts, fileFetcher, isUpdateLocksOnDemand()); ISVNEditor filterEditor = SVNAmbientDepthFilterEditor.wrap(editor, adminInfo, depthIsSticky); try { repos.update(revNumber, target, depth, sendCopyFrom, reporter, SVNCancellableEditor.newInstance(filterEditor, this, getDebugLog())); } finally { if (repos2[0] != null) { repos2[0].closeSession(); } } long targetRevision = editor.getTargetRevision(); if (targetRevision >= 0) { if ((depth == SVNDepth.INFINITY || depth == SVNDepth.UNKNOWN) && !isIgnoreExternals()) { handleExternals(wcAccess, 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 tolerating 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 { return doSwitchImpl(null, path, url, pegRevision, revision, depth, allowUnversionedObstructions, depthIsSticky); } /** * 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 tolerating 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 character; * @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 character * @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 character; * @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 character * @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(); } } /** * Sets whether keywords must be expanded during an export operation. * * @param expand * <span class="javakeyword">true</span> to expand; otherwise * <span class="javakeyword">false</span> * @since 1.3 */ public void setExportExpandsKeywords(boolean expand) { myIsExportExpandsKeywords = expand; } /** * Says whether keywords expansion during export operations is turned on or * not. * * @return <span class="javakeyword">true</span> if expanding keywords; * <span class="javakeyword">false</span> otherwise * @since 1.3 */ 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()) { 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); } if (entry.isDirectory() && entry.isThisDir()) { 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 mimeType = props.getStringPropertyValue(SVNProperty.MIME_TYPE); String charset = SVNTranslator.getCharset(charsetProp, mimeType, 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 = SVNFileUtil.getFileLastModified(adminArea.getFile(fileName)); } 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(), entry.getRepositoryRoot(), 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) { 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) { SVNFileUtil.setLastModified(dstPath, 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(), repository.getRepositoryRoot(true).toString(), 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())); SVNFileType fileType = SVNFileType.getType(dstPath); if (fileType == SVNFileType.NONE) { editor.openRoot(revNumber); } if (!isIgnoreExternals() && depth == SVNDepth.INFINITY) { Map externals = editor.getCollectedExternals(); handleExternals(null, 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(); String repositoryRoot = isExportExpandsKeywords() ? repository.getRepositoryRoot(true).toString() : null; 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"); } String mimeType = properties.getStringValue(SVNProperty.MIME_TYPE); boolean binary = SVNProperty.isBinaryMimeType(mimeType); String charset = SVNTranslator.getCharset(properties.getStringValue(SVNProperty.CHARSET), mimeType, url, getOptions()); Map keywords = SVNTranslator.computeKeywords(properties.getStringValue(SVNProperty.KEYWORDS), url, repositoryRoot, 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); 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); } } public 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; } public static SVNURL canonicalizeURL(SVNURL url, boolean omitDefaultPort) throws SVNException { if (url == null || url.getPort() <= 0) { return null; } int defaultPort = SVNURL.getDefaultPortNumber(url.getProtocol()); if (defaultPort <= 0) { return null; } if (omitDefaultPort) { if (url.hasPort() && url.getPort() == defaultPort) { return SVNURL.create(url.getProtocol(), url.getUserInfo(), url.getHost(), -1, url.getPath(), false); } } else if (!url.hasPort()) { return SVNURL.create(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), false); } return null; } private void handleExternals(SVNWCAccess wcAccess, 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()); } 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) { 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()) { continue; } String oldValue = (String) oldExternals.get(diffPath); String newValue = (String) newExternals.get(diffPath); 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(); 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]); } 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); if (!isExport) { externalDiff.ownerURL = getOwnerURL(externalDiff.owner); } if (externalDiff.ownerURL == null) { externalDiff.ownerURL = fromURL.appendPath(diffPath, false); } handleExternalItemChange(wcAccess, 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); if (!isExport) { externalDiff.ownerURL = getOwnerURL(externalDiff.owner); } if (externalDiff.ownerURL == null) { externalDiff.ownerURL = fromURL.appendPath(diffPath, false); } handleExternalItemChange(wcAccess, externalDiff.newExternal.getPath(), externalDiff); } } } } private SVNURL getOwnerURL(File root) { if (root != null && SVNFileType.getType(root) == SVNFileType.DIRECTORY) { SVNWCAccess access = createWCAccess(); try { access.open(root, false, 0); SVNEntry entry = access.getVersionedEntry(root, false); if (entry != null) { return entry.getSVNURL(); } } catch (SVNException e) { e.printStackTrace(); } finally { if (access != null) { try { access.close(); } catch (SVNException e) { } } } } return null; } private void handleExternalItemChange(SVNWCAccess access, String targetDir, ExternalDiff externalDiff) throws SVNException { try { handleExternalChange(access, targetDir, externalDiff); } catch (SVNException svne) { File target = new File(externalDiff.owner, targetDir); SVNEvent event = SVNEventFactory.createSVNEvent(target, SVNNodeKind.UNKNOWN, null, SVNRepository.INVALID_REVISION, SVNEventAction.FAILED_EXTERNAL, SVNEventAction.UPDATE_EXTERNAL, svne .getErrorMessage(), null); dispatchEvent(event); } } /** * oldURL is null when externals is added: jsvn ps svn:externals "path URL" * . jsvn up . newURL is null when external is deleted: jsvn pd * svn:externals . jsvn up . Also newURL or oldURL could be null, when * external property is added or removed by update itself (someone else has * changed it). For instance, oldURL is always null during checkout or * export operation. */ private void handleExternalChange(SVNWCAccess access, 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; SVNRepository repository = null; SVNNodeKind kind = null; SVNURL reposRootURL = null; if (newURL != null) { long[] rev = { SVNRepository.INVALID_REVISION }; repository = createRepository(newURL, null, null, externalPegRevision, externalRevision, rev); reposRootURL = repository.getRepositoryRoot(true); kind = repository.checkPath("", rev[0]); if (kind == SVNNodeKind.NONE) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_ILLEGAL_URL, "URL ''{0}'' at revision {1} doesn''t exist", new Object[] { repository.getLocation(), String.valueOf(rev[0]) }); SVNErrorManager.error(err, SVNLogType.WC); } if (kind != SVNNodeKind.DIR && kind != SVNNodeKind.FILE) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_ILLEGAL_URL, "URL ''{0}'' at revision {1} is not a file or a directory", new Object[] { repository.getLocation(), String.valueOf(rev[0]) }); SVNErrorManager.error(err, SVNLogType.WC); } } try { setEventPathPrefix("path"); if (oldURL == null) { if (kind == SVNNodeKind.DIR) { 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 (kind == SVNNodeKind.FILE) { dispatchEvent(SVNEventFactory.createSVNEvent(target, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPDATE_EXTERNAL, null, null, null)); if (externalDiff.isExport) { boolean ignoreExternals = isIgnoreExternals(); setIgnoreExternals(true); doExport(newURL, target, externalPegRevision, externalRevision, null, false, SVNDepth.INFINITY); setIgnoreExternals(ignoreExternals); } else { switchFileExternal(access, target, newURL, externalPegRevision, externalRevision, reposRootURL); } } } 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)) { if (kind == SVNNodeKind.DIR) { 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); } else { dispatchEvent(SVNEventFactory.createSVNEvent(target, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPDATE_EXTERNAL, null, null, null)); switchFileExternal(access, target, newURL, externalPegRevision, externalRevision, reposRootURL); } } } 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 switchFileExternal(SVNWCAccess wcAccess, File path, SVNURL url, SVNRevision pegRevision, SVNRevision revision, SVNURL reposRootURL) throws SVNException { String target = SVNWCManager.getActualTarget(path); File anchor = "".equals(target) ? path : path.getParentFile(); boolean closeTarget = false; boolean revertFile = false; boolean removeFromRevisionControl = false; boolean unlinkFile = false; boolean cleanUp = false; boolean ignoreExternals = isIgnoreExternals(); SVNAdminArea targetArea = null; try { try { targetArea = wcAccess.retrieve(anchor); } catch (SVNException svne) { SVNErrorMessage err = svne.getErrorMessage(); if (err.getErrorCode() == SVNErrorCode.WC_NOT_LOCKED) { SVNWCAccess targetAccess = SVNWCAccess.newInstance(null); targetArea = targetAccess.open(anchor, true, 1); closeTarget = true; SVNURL dstWCReposRootURL = getReposRoot(anchor, null, SVNRevision.BASE, targetArea, targetAccess); if (!reposRootURL.equals(dstWCReposRootURL)) { SVNErrorMessage err1 = SVNErrorMessage.create(SVNErrorCode.RA_REPOS_ROOT_URL_MISMATCH, "Cannot insert a file external from ''{0}'' into a working copy from a different repository rooted at ''{1}''", new Object[] { url, dstWCReposRootURL }); SVNErrorManager.error(err1, SVNLogType.WC); } } else { throw svne; } } if (targetArea.getFormatVersion() < SVNAdminAreaFactory.WC_FORMAT_16) { dispatchEvent(SVNEventFactory.createSVNEvent(path, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.SKIP, SVNEventAction.UPDATE_EXTERNAL, null, null)); return; } SVNEntry entry = targetArea.getEntry(target, false); if (entry != null) { if (entry.getExternalFilePath() == null) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_FILE_EXTERNAL_OVERWRITE_VERSIONED, "The file external from ''{0}'' cannot overwrite the existing versioned item at ''{1}''", new Object[] { url, path }); SVNErrorManager.error(err, SVNLogType.WC); } } else { targetArea.getVersionedEntry(targetArea.getThisDirName(), false); boolean hasPropConflicts = targetArea.hasPropConflict(targetArea.getThisDirName()); boolean hasTreeConflicts = targetArea.hasTreeConflict(targetArea.getThisDirName()); if (hasPropConflicts || hasTreeConflicts) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_FOUND_CONFLICT, "The file external from ''{0}'' cannot be written to ''{1}'' while ''{2}'' remains in conflict", new Object[] { url, path, anchor }); SVNErrorManager.error(err, SVNLogType.WC); } if (!path.exists()) { SVNFileUtil.createEmptyFile(path); unlinkFile = true; } ISVNEventHandler eventHandler = targetArea.getWCAccess().getEventHandler(); try { targetArea.getWCAccess().setEventHandler(null); SVNWCManager.add(path, targetArea, null, SVNRepository.INVALID_REVISION, SVNDepth.INFINITY); } catch (SVNException svne) { cleanUp = true; throw svne; } finally { if (eventHandler != null) { targetArea.getWCAccess().setEventHandler(eventHandler); } } revertFile = true; try { targetArea.setFileExternalLocation(target, url, pegRevision, revision, reposRootURL); } catch (SVNException svne) { cleanUp = true; throw svne; } } setIgnoreExternals(true); try { doSwitchImpl(targetArea.getWCAccess(), path, url, pegRevision, revision, SVNDepth.EMPTY, false, false); } catch (SVNException svne) { cleanUp = true; throw svne; } if (unlinkFile) { revertFile = false; removeFromRevisionControl = true; } } catch (SVNException svne) { if (cleanUp) { if (revertFile) { SVNWCClient16 wcClient = new SVNWCClient16(getRepositoryPool(), getOptions()); try { wcClient.doRevert(new File[] { path }, SVNDepth.EMPTY, null); } catch (SVNException svne2) { } } if (removeFromRevisionControl) { try { targetArea.removeFromRevisionControl(target, true, false); } catch (SVNException svne2) { } } if (unlinkFile) { try { SVNFileUtil.deleteFile(path); } catch (SVNException svne2) { } } } throw svne; } finally { setIgnoreExternals(ignoreExternals); if (closeTarget) { targetArea.getWCAccess().close(); } } } 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, validatedUUID, expectedUUID }); 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, actualUUID, expectedUUID }); 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) { 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() && childEntry.getDepth() != SVNDepth.EXCLUDE) { 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()); } } }