/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.wc;

import java.io.File;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNURLUtil;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNLog;
import org.tmatesoft.svn.core.internal.wc.admin.SVNVersionedProperties;
import org.tmatesoft.svn.core.internal.wc.admin.SVNWCAccess;
import org.tmatesoft.svn.core.internal.wc2.old.SvnOldRepositoryAccess;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNLocationEntry;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.ISVNCommitHandler;
import org.tmatesoft.svn.core.wc.ISVNCommitParameters;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNExternalsHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNRepositoryPool;
import org.tmatesoft.svn.core.wc.SVNCommitItem;
import org.tmatesoft.svn.core.wc.SVNCopySource;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import org.tmatesoft.svn.core.wc2.ISvnOperationOptionsProvider;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;
import org.tmatesoft.svn.core.internal.wc16.*;

public class SVNCopyDriver extends SVNBasicDelegate {

    private SVNWCAccess myWCAccess;
    private boolean myIsDisableLocalModificationsCopying;
    
    public void setDisableLocalModificationCopying(boolean disable) {
        myIsDisableLocalModificationsCopying = disable;
    }
    
    protected SVNCopyDriver(ISVNAuthenticationManager authManager, ISVNOptions options) {
        super(authManager, options);
    }

    protected SVNCopyDriver(ISVNRepositoryPool repositoryPool, ISVNOptions options) {
        super(repositoryPool, options);
    }

    protected void setWCAccess(SVNWCAccess access) {
        myWCAccess = access;
    }

    private SVNWCAccess getWCAccess() {
        if (myWCAccess == null) {
            return createWCAccess();
        }
        return myWCAccess;
    }

    private SVNAdminArea probeOpen(SVNWCAccess access, File path, boolean writeLock, int depth) throws SVNException {
        if (access != myWCAccess) {
            return access.probeOpen(path, writeLock, depth);
        }
        return myWCAccess.probeRetrieve(path);
    }

    private SVNAdminArea open(SVNWCAccess access, File path, boolean writeLock, boolean stealLock, int depth) throws SVNException {
        if (access != myWCAccess) {
            return access.open(path, writeLock, stealLock, depth);
        }
        return myWCAccess.retrieve(path);
    }

    private void close(SVNWCAccess access) throws SVNException {
        if (access != myWCAccess) {
            access.close();
        }
    }

    protected SVNCopySource[] expandCopySources(SVNCopySource[] sources) throws SVNException {
        Collection expanded = new ArrayList(sources.length);
        for (int i = 0; i < sources.length; i++) {
            SVNCopySource source = sources[i];
            if (source.isCopyContents() && source.isURL()) {
                // get children at revision.
                SVNRevision pegRevision = source.getPegRevision();
                if (!pegRevision.isValid()) {
                    pegRevision = SVNRevision.HEAD;
                }
                SVNRevision startRevision = source.getRevision();
                if (!startRevision.isValid()) {
                    startRevision = pegRevision;
                }
                SVNRepositoryLocation[] locations = getLocations(source.getURL(), null, null, pegRevision, startRevision, SVNRevision.UNDEFINED);
                SVNRepository repository = createRepository(locations[0].getURL(), null, null, true);
                long revision = locations[0].getRevisionNumber();
                Collection entries = new ArrayList();
                repository.getDir("", revision, null, 0, entries);
                for (Iterator ents = entries.iterator(); ents.hasNext();) {
                    SVNDirEntry entry = (SVNDirEntry) ents.next();
                    // add new copy source.
                    expanded.add(new SVNCopySource(SVNRevision.UNDEFINED, source.getRevision(), entry.getURL()));
                }
            } else {
                expanded.add(source);
            }
        }
        return (SVNCopySource[]) expanded.toArray(new SVNCopySource[expanded.size()]);
    }

    private SVNCommitInfo copyReposToRepos(List copyPairs, boolean makeParents, boolean isMove, boolean pinExternals, Map<SvnTarget, List<SVNExternal>> externalsToPin, String message, SVNProperties revprops, ISVNCommitHandler commitHandler) throws SVNException {
        List pathInfos = new ArrayList();
        List pinExternalsOnlyInfos = new ArrayList();
        Map pathsMap = new SVNHashMap();
        for (int i = 0; i < copyPairs.size(); i++) {
            CopyPathInfo info = new CopyPathInfo();
            pathInfos.add(info);
        }
        String commonURL = ((CopyPair) copyPairs.get(0)).mySource;
        String topDstURL = ((CopyPair) copyPairs.get(0)).myDst;
        for (int i = 1; i < copyPairs.size(); i++) {
            CopyPair pair = (CopyPair) copyPairs.get(i);
            commonURL = SVNPathUtil.getCommonPathAncestor(commonURL, pair.mySource);
        }
        
        if (copyPairs.size() == 1) {
            commonURL = SVNPathUtil.getCommonPathAncestor(commonURL, topDstURL);
        } else {
            commonURL = SVNPathUtil.getCommonPathAncestor(commonURL, SVNPathUtil.removeTail(topDstURL));
        }
        
        try {
            SVNURL.parseURIEncoded(commonURL);
        } catch (SVNException e) {
            commonURL = null;
        }
        if (commonURL == null) {
            SVNURL url1 = SVNURL.parseURIEncoded(((CopyPair) copyPairs.get(0)).mySource);
            SVNURL url2 = SVNURL.parseURIEncoded(((CopyPair) copyPairs.get(0)).myDst);
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Source and dest appear not to be in the same repository (src: ''{0}''; dst: ''{1}'')", new Object[] {url1, url2});
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        SVNRepository nonTopRepos = null;
        for (int i = 0; i < copyPairs.size(); i++) {
            CopyPair pair = (CopyPair) copyPairs.get(i);
            CopyPathInfo info = (CopyPathInfo) pathInfos.get(i);
            if (nonTopRepos == null) {
                nonTopRepos = createRepository(SVNURL.parseURIEncoded(pair.mySource), null, null, true);
            }
            if (pair.mySource.equals(pair.myDst)) {
                info.isResurrection = true;
            }
        }
        /*
         * Get list of parents that have to be created. start with first 'dst' parent.
         * This is a list of urls.
         */        
        String rootURL = nonTopRepos.getRepositoryRoot(true).toString();

        List newDirs = new ArrayList();
        SVNURL oldLocation = null;
        if (makeParents) {
            CopyPair pair = (CopyPair) copyPairs.get(0);
            if (!pair.myDst.equals(rootURL)) {
                oldLocation = nonTopRepos.getLocation();
                nonTopRepos.setLocation(SVNURL.parseURIEncoded(pair.myDst).removePathTail(), false);
                SVNNodeKind kind = nonTopRepos.checkPath("", -1);
                while (kind == SVNNodeKind.NONE) {
                    newDirs.add(nonTopRepos.getLocation().toString());
                    nonTopRepos.setLocation(nonTopRepos.getLocation().removePathTail(), false);
                    kind = nonTopRepos.checkPath("", -1);
                }
            }
        } else if (Boolean.getBoolean("svnkit.compatibleHash")) {            
            // XXX: hack for tests to generate error message tests will like.
            // do not check paths above repository root.
            CopyPair pair = (CopyPair) copyPairs.get(0);
            if (!pair.myDst.equals(rootURL)) {
                oldLocation = nonTopRepos.getLocation();
                nonTopRepos.setLocation(SVNURL.parseURIEncoded(pair.myDst).removePathTail(), false);
                SVNNodeKind kind = nonTopRepos.checkPath("", -1);
                if (kind == SVNNodeKind.NONE) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "''{0}'' path not found", nonTopRepos.getLocation());
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
        }
        if (oldLocation != null) {
            nonTopRepos.setLocation(oldLocation, false);
        }

        /*
         * Check if source is dst child (while dst is not root).
         */
        for (int i = 0; i < copyPairs.size(); i++) {
            CopyPair pair = (CopyPair) copyPairs.get(i);
            CopyPathInfo info = (CopyPathInfo) pathInfos.get(i);
            if (!pair.myDst.equals(rootURL) && SVNPathUtil.getPathAsChild(pair.myDst, pair.mySource) != null) {
                info.isResurrection = true;
            }
        }

        long latestRevision = nonTopRepos.getLatestRevision();

        for (int i = 0; i < copyPairs.size(); i++) {
            CopyPair pair = (CopyPair) copyPairs.get(i);
            CopyPathInfo info = (CopyPathInfo) pathInfos.get(i);
            pair.mySourceRevisionNumber = getRevisionNumber(pair.mySourceRevision, nonTopRepos, null);
            info.mySourceRevisionNumber = pair.mySourceRevisionNumber;

            SVNRepositoryLocation[] locations = getLocations(SVNURL.parseURIEncoded(pair.mySource), null, null /*optimize topRepos*/, pair.mySourcePegRevision, pair.mySourceRevision, SVNRevision.UNDEFINED);
            pair.mySource = locations[0].getURL().toString();
            
            // tests:
            // src is equal to dst
            if (isMove && pair.mySource.equals(pair.myDst)) {
            //if ("".equals(srcRelative) && isMove) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Cannot move URL ''{0}'' into itself", SVNURL.parseURIEncoded(pair.mySource));
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            // src doesn't exist at source revision.
            nonTopRepos.setLocation(SVNURL.parseURIEncoded(pair.mySource), false);
            info.mySourceKind = nonTopRepos.checkPath("", pair.mySourceRevisionNumber);
            if (info.mySourceKind == SVNNodeKind.NONE) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND,
                        "Path ''{0}'' does not exist in revision {1}", new Object[] {SVNURL.parseURIEncoded(pair.mySource), new Long(pair.mySourceRevisionNumber)});
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            // dst already exists at HEAD.
            nonTopRepos.setLocation(SVNURL.parseURIEncoded(pair.myDst), false);
            SVNNodeKind dstKind = nonTopRepos.checkPath("", latestRevision);
            if (dstKind != SVNNodeKind.NONE) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_ALREADY_EXISTS,
                        "Path ''{0}'' already exists", pair.myDst);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            info.mySource = pair.mySource;
            info.myDstPath = pair.myDst;

            if (pinExternals) {
                final ISVNAuthenticationManager authenticationManager = nonTopRepos.getAuthenticationManager();

                SvnOldRepositoryAccess oldRepositoryAccess = new SvnOldRepositoryAccess(new ISvnOperationOptionsProvider() {
                    public ISVNEventHandler getEventHandler() {
                        return SVNCopyDriver.this;
                    }
                    public ISVNOptions getOptions() {
                        return SVNCopyDriver.this.getOptions();
                    }
                    public ISVNRepositoryPool getRepositoryPool() {
                        return SVNCopyDriver.this.getRepositoryPool();
                    }
                    public ISVNAuthenticationManager getAuthenticationManager() {
                        return authenticationManager;
                    }
                    public ISVNCanceller getCanceller() {
                        return getEventHandler();
                    }
                });

                Map<String, SVNPropertyValue> pinnedExternals = SVNExternalsUtil.resolvePinnedExternals(null, oldRepositoryAccess, externalsToPin, SvnTarget.fromURL(SVNURL.parseURIEncoded(pair.mySource), pair.mySourcePegRevision), SvnTarget.fromURL(SVNURL.parseURIEncoded(pair.myDst)), pair.mySourceRevisionNumber, nonTopRepos, SVNURL.parseURIEncoded(rootURL));
                queueExternalsChangePathInfos(pinExternalsOnlyInfos, pathInfos, pinnedExternals, info);
            }
        }

        List paths = new ArrayList(copyPairs.size() * 2);
        List commitItems = new ArrayList(copyPairs.size() * 2);
        if (makeParents) {
            for (Iterator newDirsIter = newDirs.iterator(); newDirsIter.hasNext();) {
                String itemURL = (String) newDirsIter.next();
                SVNCommitItem item = new SVNCommitItem(null, 
                        SVNURL.parseURIEncoded(itemURL), null, SVNNodeKind.NONE, null, null, true, false, false, false, false, false);
                commitItems.add(item);
            }
        }

        for (Iterator infos = pathInfos.iterator(); infos.hasNext();) {
            CopyPathInfo info = (CopyPathInfo) infos.next();
            SVNURL itemURL = SVNURL.parseURIEncoded(info.myDstPath);
            SVNCommitItem item = new SVNCommitItem(null, itemURL, null, SVNNodeKind.NONE, null, null, true, false, false, false, false, false);
            commitItems.add(item);
            pathsMap.put(info.myDstPath, info);
            if (isMove && !info.isResurrection) {
                itemURL = SVNURL.parseURIEncoded(info.mySource);
                item = new SVNCommitItem(null, itemURL, null, SVNNodeKind.NONE, null, null, false, true, false, false, false, false);
                commitItems.add(item);
                pathsMap.put(info.mySource, info);
            }
        }

        if (makeParents) {
            for (Iterator newDirsIter = newDirs.iterator(); newDirsIter.hasNext();) {
                String dirPath = (String) newDirsIter.next();
                CopyPathInfo info = new CopyPathInfo();
                info.myDstPath = dirPath;
                info.isDirAdded = true;
                paths.add(info.myDstPath);
                pathsMap.put(dirPath, info);
            }
        }

        for (Iterator infos = pathInfos.iterator(); infos.hasNext();) {
            CopyPathInfo info = (CopyPathInfo) infos.next();
            nonTopRepos.setLocation(SVNURL.parseURIEncoded(info.mySource), false);
            Map mergeInfo = calculateTargetMergeInfo(null, null, SVNURL.parseURIEncoded(info.mySource),
                    info.mySourceRevisionNumber, nonTopRepos, false);
            if (mergeInfo != null) {
                info.myMergeInfoProp = SVNMergeInfoUtil.formatMergeInfoToString(mergeInfo, null);
            }
            paths.add(info.myDstPath);
            if (isMove && !info.isResurrection) {
                // this is too.
                paths.add(info.mySource);
            }
        }

        if (pinExternalsOnlyInfos != null) {
            for (Iterator iterator = pinExternalsOnlyInfos.iterator(); iterator.hasNext(); ) {
                final CopyPathInfo info = (CopyPathInfo) iterator.next();
                paths.add(info.myDstPath);
                pathsMap.put(info.myDstPath, info);
            }
        }


        SVNCommitItem[] commitables = (SVNCommitItem[]) commitItems.toArray(new SVNCommitItem[commitItems.size()]);
        message = commitHandler.getCommitMessage(message, commitables);
        if (message == null) {
            return SVNCommitInfo.NULL;
        }
        message = SVNCommitUtil.validateCommitMessage(message);

        revprops = commitHandler.getRevisionProperties(message, commitables, revprops == null ? new SVNProperties() : revprops);
        if (revprops == null) {
            return SVNCommitInfo.NULL;
        }

        SVNPropertiesManager.validateRevisionProperties(revprops);

        SVNURL topURL = SVNURL.parseURIEncoded((String) paths.get(0));
        for(int i = 1; i < paths.size(); i++) {
            String url = (String) paths.get(i);
            topURL = SVNURLUtil.getCommonURLAncestor(topURL, SVNURL.parseURIEncoded(url));
        }
        if (paths.contains(topURL.toString())) {
            topURL = topURL.removePathTail();
        }
        for(int i = 0; i < paths.size(); i++) {
            String url = (String) paths.get(i);
            SVNURL svnURL =  SVNURL.parseURIEncoded(url);
            url = SVNPathUtil.getPathAsChild(topURL.getPath(), svnURL.getPath());
            paths.set(i, url);
            CopyPathInfo info = (CopyPathInfo) pathsMap.remove(svnURL.toString());
            if (info != null) {
                if (info.mySource != null) {
                    info.mySourcePath = getPathRelativeToRoot(null, SVNURL.parseURIEncoded(info.mySource), SVNURL.parseURIEncoded(rootURL), null, null);
                    info.mySourceRelativePath = getPathRelativeToSession(SVNURL.parseURIEncoded(info.mySource), topURL, null);
                }
                pathsMap.put(url, info);
            }
            
        }
        
        nonTopRepos.setLocation(topURL, false);
        ISVNEditor commitEditor = nonTopRepos.getCommitEditor(message, null, true, revprops, null);
        ISVNCommitPathHandler committer = new CopyCommitPathHandler(pathsMap, isMove);

        SVNCommitInfo result = null;
        try {
            SVNCommitUtil.driveCommitEditor(committer, paths, commitEditor, latestRevision);
            SVNEvent event = SVNEventFactory.createSVNEvent(null, SVNNodeKind.UNKNOWN, null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_FINALIZING, SVNEventAction.COMMIT_FINALIZING, null, null);
            event.setURL(topURL);
            handleEvent(event, -1);
            result = commitEditor.closeEdit();
        } catch (SVNCancelException cancel) {
            throw cancel;
        } catch (SVNException e) {
            SVNErrorMessage err = e.getErrorMessage().wrap("Commit failed (details follow):");
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        } finally {
            if (commitEditor != null && result == null) {
                try {
                    commitEditor.abortEdit();
                } catch (SVNException e) {
                    SVNDebugLog.getDefaultLog().logFine(SVNLogType.WC, e);
                }
            }
        }
        if (result != null && result.getNewRevision() >= 0) {
            dispatchEvent(SVNEventFactory.createSVNEvent(null, SVNNodeKind.NONE, null, result.getNewRevision(), SVNEventAction.COMMIT_COMPLETED, null, null, null), ISVNEventHandler.UNKNOWN);
        }
        return result != null ? result : SVNCommitInfo.NULL;
    }

    private void queueExternalsChangePathInfos(List<CopyPathInfo> newPathInfos, List<CopyPathInfo> pathInfos, Map<String, SVNPropertyValue> pinnedExternals, CopyPathInfo parentInfo) throws SVNException {
        for (Map.Entry<String, SVNPropertyValue> entry : pinnedExternals.entrySet()) {
            String dstRelpath = entry.getKey();
            SVNPropertyValue externalsProperyValue = entry.getValue();

            SVNURL srcUrl = SVNURL.parseURIEncoded(parentInfo.mySource).appendPath(dstRelpath, false);

            CopyPathInfo info = null;
            for (CopyPathInfo existingInfo : pathInfos) {
                if (srcUrl.toString().equals(existingInfo.mySource)) {
                    info = existingInfo;
                    break;
                }
            }
            if (info == null) {
                info = new CopyPathInfo();
                info.mySource = srcUrl.toString();
                info.mySourcePath = null;
                info.myDstPath = SVNPathUtil.append(parentInfo.myDstPath, dstRelpath);
                info.mySourceKind = SVNNodeKind.DIR;
                info.myIsOnlyPinExternals = true;
                newPathInfos.add(info);
            }
            info.myExternalsProp = SVNPropertyValue.getPropertyAsString(externalsProperyValue);
        }
    }

    private String getUUIDFromPath(SVNWCAccess wcAccess, File path) throws SVNException {
        SVNEntry entry = wcAccess.getVersionedEntry(path, true);
        String uuid = null;
        if (entry.getUUID() != null) {
            uuid = entry.getUUID();
        } else if (entry.getURL() != null) {
            SVNRepository repos = createRepository(entry.getSVNURL(), null, null, false);
            try {
                uuid = repos.getRepositoryUUID(true);
            } finally {
                repos.closeSession();
            }
        } else {
            if (wcAccess.isWCRoot(path)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "''{0}'' has no URL", path);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            uuid = getUUIDFromPath(wcAccess, path.getParentFile());
        }
        return uuid;
    }

    private static void postCopyCleanup(SVNAdminArea dir) throws SVNException {
        SVNPropertiesManager.deleteWCProperties(dir, null, false);
        SVNFileUtil.setHidden(dir.getAdminDirectory(), true);
        Map attributes = new SVNHashMap();
        boolean save = false;

        for(Iterator entries = dir.entries(true); entries.hasNext();) {
            SVNEntry entry = (SVNEntry) entries.next();
            boolean deleted = entry.isDeleted();
            SVNNodeKind kind = entry.getKind();
            boolean force = false;
            if (entry.getDepth() == SVNDepth.EXCLUDE) {
                continue;
            }

            if (entry.isDeleted()) {
                force = true;
                attributes.put(SVNProperty.SCHEDULE, SVNProperty.SCHEDULE_DELETE);
                attributes.put(SVNProperty.DELETED, null);
                if (entry.isDirectory()) {
                    attributes.put(SVNProperty.KIND, SVNProperty.KIND_FILE);
                }
            }
            if (entry.getLockToken() != null) {
                force = true;
                attributes.put(SVNProperty.LOCK_TOKEN, null);
                attributes.put(SVNProperty.LOCK_OWNER, null);
                attributes.put(SVNProperty.LOCK_CREATION_DATE, null);
            }
            if (force) {
                dir.modifyEntry(entry.getName(), attributes, false, force);
                save = true;
            }
            if (!deleted && kind == SVNNodeKind.DIR && !dir.getThisDirName().equals(entry.getName())) {
                SVNAdminArea childDir = dir.getWCAccess().retrieve(dir.getFile(entry.getName()));
                postCopyCleanup(childDir);
            }

            attributes.clear();
        }

        if (save) {
            dir.saveEntries(false);
        }
    }

    protected SVNCommitInfo setupCopy(SVNCopySource[] sources, SVNPath dst, boolean isMove, boolean makeParents, boolean pinExternals, Map<SvnTarget, List<SVNExternal>> externalsToPin,
            String message, SVNProperties revprops, ISVNCommitHandler commitHandler, ISVNCommitParameters commitParameters, ISVNExternalsHandler externalsHandler) throws SVNException {
        List pairs = new ArrayList(sources.length);
        for (int i = 0; i < sources.length; i++) {
            SVNCopySource source = sources[i];
            if (source.isURL() &&
                 (source.getPegRevision() == SVNRevision.BASE ||
                  source.getPegRevision() == SVNRevision.COMMITTED ||
                  source.getPegRevision() == SVNRevision.PREVIOUS)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION,
                        "Revision type requires a working copy path, not URL");
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        boolean srcIsURL = sources[0].isURL();
        boolean dstIsURL = dst.isURL();

        if (sources.length > 1) {
            for (int i = 0; i < sources.length; i++) {
                SVNCopySource source = sources[i];
                CopyPair pair = new CopyPair();
                pair.mySource = source.isURL() ? source.getURL().toString() : source.getFile().getAbsolutePath().replace(File.separatorChar, '/');
                pair.setSourceRevisions(source.getPegRevision(), source.getRevision());
                if (SVNPathUtil.isURL(pair.mySource) != srcIsURL) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                            "Cannot mix repository and working copy sources");
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                String baseName = source.getName();
                if (srcIsURL && !dstIsURL) {
                    baseName = SVNEncodingUtil.uriDecode(baseName);
                }
                pair.myDst = dstIsURL ? dst.getURL().appendPath(baseName, true).toString() :
                    new File(dst.getFile(), baseName).getAbsolutePath().replace(File.separatorChar, '/');
                pairs.add(pair);
            }
        } else {
            SVNCopySource source = sources[0];
            CopyPair pair = new CopyPair();
            pair.mySource = source.isURL() ? source.getURL().toString() : source.getFile().getAbsolutePath().replace(File.separatorChar, '/');
            pair.setSourceRevisions(source.getPegRevision(), source.getRevision());
            pair.myDst = dstIsURL ? dst.getURL().toString() : dst.getFile().getAbsolutePath().replace(File.separatorChar, '/');
            pairs.add(pair);
        }

        if (!srcIsURL && !dstIsURL) {
            for (Iterator ps = pairs.iterator(); ps.hasNext();) {
                CopyPair pair = (CopyPair) ps.next();
                String srcPath = pair.mySource;
                String dstPath = pair.myDst;
                if (SVNPathUtil.isAncestor(srcPath, dstPath)) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                            "Cannot copy path ''{0}'' into its own child ''{1}",
                            new Object[] { srcPath, dstPath });
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
        }
        
        if (isMove && !srcIsURL) {
            for (Iterator ps = pairs.iterator(); ps.hasNext();) {
                CopyPair pair = (CopyPair) ps.next();
                SVNWCAccess wcAccess = getWCAccess();
                try {
                    File srcFile = new File(pair.mySource);
                    probeOpen(wcAccess, srcFile, false, 0);
                    SVNEntry entry = wcAccess.getVersionedEntry(new File(pair.mySource), false);
                    if (entry.getExternalFilePath() != null) {
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CANNOT_MOVE_FILE_EXTERNAL, 
                                "Cannot move the file external at ''{0}''; please propedit the svn:externals description that created it", 
                                srcFile);
                        SVNErrorManager.error(err, SVNLogType.WC);
                    }
                } finally {
                    close(wcAccess);
                }
            }
        }
        
        if (isMove) {
            if (srcIsURL == dstIsURL) {
                for (Iterator ps = pairs.iterator(); ps.hasNext();) {
                    CopyPair pair = (CopyPair) ps.next();
                    boolean same;
                    Object p;
                    if (!srcIsURL) {
                        File srcPath = new File(pair.mySource);
                        File dstPath = new File(pair.myDst);
                        same = srcPath.equals(dstPath);
                        p = srcPath;
                    } else {
                        SVNURL srcURL = SVNURL.parseURIEncoded(pair.mySource);
                        SVNURL dstURL = SVNURL.parseURIEncoded(pair.myDst);
                        same = srcURL.getPath().equals(dstURL.getPath());
                        p = srcURL;
                    }
                    if (same) {
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                                "Cannot move {1} ''{0}'' into itself", p, srcIsURL ? "URL" : "path");
                        SVNErrorManager.error(err, SVNLogType.WC);
                    }
                }
            } else {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                        "Moves between the working copy and the repository are not supported");
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        } else {
            if (!srcIsURL) {
                boolean needReposRevision = false;
                boolean needReposPegRevision = false;
                for (Iterator ps = pairs.iterator(); ps.hasNext();) {
                    CopyPair pair = (CopyPair) ps.next();
                    if (pair.mySourceRevision != SVNRevision.UNDEFINED &&
                            pair.mySourceRevision != SVNRevision.WORKING) {
                        needReposRevision = true;
                    }
                    if (pair.mySourcePegRevision != SVNRevision.UNDEFINED &&
                            pair.mySourcePegRevision != SVNRevision.WORKING) {
                        needReposPegRevision = true;
                    }
                    if (needReposRevision || needReposPegRevision) {
                        break;
                    }
                }

                if (needReposRevision || needReposPegRevision) {
                    for (Iterator ps = pairs.iterator(); ps.hasNext();) {
                        CopyPair pair = (CopyPair) ps.next();
                        SVNWCAccess wcAccess = getWCAccess();
                        try {
                            probeOpen(wcAccess, new File(pair.mySource), false, 0);
                            SVNEntry entry = wcAccess.getEntry(new File(pair.mySource), false);
                            SVNURL url = entry.isCopied() ? entry.getCopyFromSVNURL() : entry.getSVNURL();
                            if (url == null) {
                                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL,
                                        "''{0}'' does not have a URL associated with it", new File(pair.mySource));
                                SVNErrorManager.error(err, SVNLogType.WC);
                            }
                            pair.mySource = url.toString();
                            if (!needReposPegRevision || pair.mySourcePegRevision == SVNRevision.BASE) {
                                pair.mySourcePegRevision = entry.isCopied() ? SVNRevision.create(entry.getCopyFromRevision()) : SVNRevision.create(entry.getRevision());
                            }
                            if (pair.mySourceRevision == SVNRevision.BASE) {
                                pair.mySourceRevision = entry.isCopied() ? SVNRevision.create(entry.getCopyFromRevision()) : SVNRevision.create(entry.getRevision());
                            }
                        } finally {
                            close(wcAccess);
                        }
                    }
                    srcIsURL = true;
                }
            }
        }
        if (!srcIsURL && !dstIsURL) {
            copyWCToWC(pairs, isMove, makeParents);
            return SVNCommitInfo.NULL;
        } else if (!srcIsURL && dstIsURL) {
            //wc2url.
            return copyWCToRepos(pairs, makeParents, message, revprops, commitHandler, commitParameters, externalsHandler);
        } else if (srcIsURL && !dstIsURL) {
            // url2wc.
            copyReposToWC(pairs, makeParents);
            return SVNCommitInfo.NULL;
        } else {
            return copyReposToRepos(pairs, makeParents, isMove, pinExternals, externalsToPin, message, revprops, commitHandler);
        }
    }

    private SVNCommitInfo copyWCToRepos(List copyPairs, boolean makeParents, String message,
            SVNProperties revprops, ISVNCommitHandler commitHandler, ISVNCommitParameters commitParameters, ISVNExternalsHandler externalsHandler) throws SVNException {
        String topSrc = ((CopyPair) copyPairs.get(0)).mySource;
        for (int i = 1; i < copyPairs.size(); i++) {
            CopyPair pair = (CopyPair) copyPairs.get(i);
            topSrc = SVNPathUtil.getCommonPathAncestor(topSrc, pair.mySource);
        }
        SVNWCAccess wcAccess = createWCAccess();
        SVNCommitInfo info = null;
        ISVNEditor commitEditor = null;
        Collection tmpFiles = null;
        try {
            SVNAdminArea adminArea = wcAccess.probeOpen(new File(topSrc), false, SVNWCAccess.INFINITE_DEPTH);
            wcAccess.setAnchor(adminArea.getRoot());

            String topDstURL = ((CopyPair) copyPairs.get(0)).myDst;
            topDstURL = SVNPathUtil.removeTail(topDstURL);
            for (int i = 1; i < copyPairs.size(); i++) {
                CopyPair pair = (CopyPair) copyPairs.get(i);
                topDstURL = SVNPathUtil.getCommonPathAncestor(topDstURL, pair.myDst);
            }

            // should we use also wcAccess here? i do not think so.
            SVNRepository repos = createRepository(SVNURL.parseURIEncoded(topDstURL), adminArea.getRoot(),
                    wcAccess, true);
            List newDirs = new ArrayList();
            if (makeParents) {
                String rootURL = topDstURL;
                SVNNodeKind kind = repos.checkPath("", -1);
                while(kind == SVNNodeKind.NONE) {
                    newDirs.add(rootURL);
                    rootURL = SVNPathUtil.removeTail(rootURL);
                    repos.setLocation(SVNURL.parseURIEncoded(rootURL), false);
                    kind = repos.checkPath("", -1);
                }
                topDstURL = rootURL;
            }

            for (int i = 0; i < copyPairs.size(); i++) {
                CopyPair pair = (CopyPair) copyPairs.get(i);
                SVNEntry entry = wcAccess.getEntry(new File(pair.mySource), false);
                pair.mySourceRevisionNumber = entry.getRevision();
                String dstRelativePath = SVNPathUtil.getPathAsChild(topDstURL, pair.myDst);
                dstRelativePath = SVNEncodingUtil.uriDecode(dstRelativePath);
                SVNNodeKind kind = repos.checkPath(dstRelativePath, -1);
                if (kind != SVNNodeKind.NONE) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_ALREADY_EXISTS,
                            "Path ''{0}'' already exists", SVNURL.parseURIEncoded(pair.myDst));
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
            // create commit items list to fetch log messages.
            List commitItems = new ArrayList(copyPairs.size());
            if (makeParents) {
                for (int i = 0; i < newDirs.size(); i++) {
                    String newDirURL = (String) newDirs.get(i);
                    SVNURL url = SVNURL.parseURIEncoded(newDirURL);
                    SVNCommitItem item = new SVNCommitItem(null, url, null, SVNNodeKind.NONE, null, null, true, false, false, false, false, false);
                    commitItems.add(item);
                }
            }
            for (int i = 0; i < copyPairs.size(); i++) {
                CopyPair pair = (CopyPair) copyPairs.get(i);
                SVNURL url = SVNURL.parseURIEncoded(pair.myDst);
                SVNCommitItem item = new SVNCommitItem(null, url, null, SVNNodeKind.NONE, null, null, true, false, false,
                        false, false, false);
                commitItems.add(item);
            }
            SVNCommitItem[] commitables = (SVNCommitItem[]) commitItems.toArray(new SVNCommitItem[commitItems.size()]);
            message = commitHandler.getCommitMessage(message, commitables);
            if (message == null) {
                return SVNCommitInfo.NULL;
            }
            message = SVNCommitUtil.validateCommitMessage(message);

            revprops = commitHandler.getRevisionProperties(message, commitables, revprops == null ? new SVNProperties() : revprops);
            if (revprops == null) {
                return SVNCommitInfo.NULL;
            }

            Map allCommitables = new TreeMap(SVNCommitUtil.FILE_COMPARATOR);
            repos.setLocation(repos.getRepositoryRoot(true), false);
            Map pathsToExternalsProps = new SVNHashMap();
            for (int i = 0; i < copyPairs.size(); i++) {
                CopyPair source = (CopyPair) copyPairs.get(i);
                File srcFile = new File(source.mySource);
                SVNEntry entry = wcAccess.getVersionedEntry(srcFile, false);
                SVNAdminArea dirArea;
                if (entry.isDirectory()) {
                    dirArea = wcAccess.retrieve(srcFile);
                } else {
                    dirArea = wcAccess.retrieve(srcFile.getParentFile());
                }


                pathsToExternalsProps.clear();

                Map sourceCommittables = new HashMap();
                SVNCommitUtil.harvestCommitables(sourceCommittables, dirArea, srcFile,
                        null, entry, source.myDst, entry.getURL(), true, false, false, null, SVNDepth.INFINITY,
                        false, null, commitParameters, pathsToExternalsProps);
                
                // filter out file externals.
                // path of the source relative to wcAccess anchor.
                String basePath = SVNPathUtil.canonicalizePath(wcAccess.getAnchor().getAbsolutePath());
                String sourcePath = SVNPathUtil.canonicalizePath(srcFile.getAbsolutePath());
                String path = SVNPathUtil.getRelativePath(basePath, sourcePath);
                SVNCommitUtil.filterOutFileExternals(Collections.singletonList(path), sourceCommittables, wcAccess);
                
                allCommitables.putAll(sourceCommittables);

                SVNCommitItem item = (SVNCommitItem) allCommitables.get(srcFile);
                SVNURL srcURL = entry.getSVNURL();

                Map mergeInfo = calculateTargetMergeInfo(srcFile, wcAccess, srcURL,
                        source.mySourceRevisionNumber, repos, false);

                Map wcMergeInfo = SVNPropertiesManager.parseMergeInfo(srcFile, entry, false);
                if (wcMergeInfo != null && mergeInfo != null) {
                    mergeInfo = SVNMergeInfoUtil.mergeMergeInfos(mergeInfo, wcMergeInfo);
                } else if (mergeInfo == null) {
                    mergeInfo = wcMergeInfo;
                }
                if (mergeInfo != null) {
                    String mergeInfoString = SVNMergeInfoUtil.formatMergeInfoToString(mergeInfo, null);
                    setCommitItemProperty(item, SVNProperty.MERGE_INFO, SVNPropertyValue.create(mergeInfoString));
                }

                if (!pathsToExternalsProps.isEmpty()) {
                    LinkedList newExternals = new LinkedList();
                    for (Iterator pathsIter = pathsToExternalsProps.keySet().iterator(); pathsIter.hasNext();) {
                        File localPath = (File) pathsIter.next();
                        String externalsPropString = (String) pathsToExternalsProps.get(localPath);
                        SVNExternal[] externals = SVNExternal.parseExternals(localPath.getAbsolutePath(),
                                externalsPropString);
                        boolean introduceVirtualExternalChange = false;
                        newExternals.clear();
                        for (int k = 0; k < externals.length; k++) {
                            File externalWC = new File(localPath, externals[k].getPath());
                            SVNEntry externalEntry = null;
                            SVNRevision externalsWCRevision = SVNRevision.UNDEFINED;

                            try {
                                wcAccess.open(externalWC, false, 0);
                                externalEntry = wcAccess.getEntry(externalWC, false);
                            } catch (SVNException svne) {
                                if (svne instanceof SVNCancelException) {
                                    throw svne;
                                }
                            } finally {
                                wcAccess.closeAdminArea(externalWC);
                            }
                            
                            if (externalEntry == null) {
                                externalEntry = wcAccess.getEntry(externalWC, false);
                            }                            
                            if (externalEntry != null && (externalEntry.isThisDir() || externalEntry.getExternalFilePath() != null)) {
                                externalsWCRevision = SVNRevision.create(externalEntry.getRevision());
                            }
                            
                            SVNEntry ownerEntry = wcAccess.getEntry(localPath, false);
                            SVNURL ownerURL = null;
                            if (ownerEntry != null) {
                                ownerURL = ownerEntry.getSVNURL();
                            }
                            if (ownerURL == null) {
                                // there is no entry for the directory that has external
                                // property or no url in it?
                                continue;
                            }
                            
                            SVNURL resolvedURL = externals[k].resolveURL(repos.getRepositoryRoot(true), ownerURL);
                            String unresolvedURL = externals[k].getUnresolvedUrl();
                            if (unresolvedURL != null && !SVNPathUtil.isURL(unresolvedURL) && unresolvedURL.startsWith("../"))  {
                                unresolvedURL = SVNURLUtil.getRelativeURL(repos.getRepositoryRoot(true), resolvedURL, true);
                                if (unresolvedURL.startsWith("/")) {
                                    unresolvedURL = "^" + unresolvedURL;
                                } else {
                                    unresolvedURL = "^/" + unresolvedURL;
                                }
                            }

                            SVNRevision[] revs = externalsHandler.handleExternal(
                                    externalWC,
                                    resolvedURL,
                                    externals[k].getRevision(),
                                    externals[k].getPegRevision(),
                                    externals[k].getRawValue(),
                                    externalsWCRevision);

                            if (revs != null && revs[0].equals(externals[k].getRevision())) {
                                newExternals.add(externals[k].getRawValue());
                            } else if (revs != null) {                                
                                SVNExternal newExternal = new SVNExternal(externals[k].getPath(),
                                        unresolvedURL,
                                        revs[1],
                                        revs[0], 
                                        true, 
                                        externals[k].isPegRevisionExplicit(),
                                        externals[k].isNewFormat());

                                newExternals.add(newExternal.toString());

                                if (!introduceVirtualExternalChange) {
                                    introduceVirtualExternalChange = true;
                                }
                            }
                        }

                        if (introduceVirtualExternalChange) {
                            String newExternalsProp = "";
                            for (Iterator externalsIter = newExternals.iterator(); externalsIter.hasNext();) {
                                String external = (String) externalsIter.next();
                                newExternalsProp += external + '\n';
                            }

                            SVNCommitItem itemWithExternalsChanges = (SVNCommitItem) allCommitables.get(localPath);
                            if (itemWithExternalsChanges != null) {
                                setCommitItemProperty(itemWithExternalsChanges, SVNProperty.EXTERNALS, SVNPropertyValue.create(newExternalsProp));
                            } else {
                                SVNAdminArea childArea = wcAccess.retrieve(localPath);
                                String relativePath = childArea.getRelativePath(dirArea);
                                String itemURL = SVNPathUtil.append(source.myDst,
                                        SVNEncodingUtil.uriEncode(relativePath));
                                itemWithExternalsChanges = new SVNCommitItem(localPath,
                                        SVNURL.parseURIEncoded(itemURL), null, SVNNodeKind.DIR, null, null,
                                        false, false, true, false, false, false);
                                setCommitItemProperty(itemWithExternalsChanges, SVNProperty.EXTERNALS, SVNPropertyValue.create(newExternalsProp));
                                allCommitables.put(localPath, itemWithExternalsChanges);
                            }
                        }
                    }
                }
            }

            commitItems = new ArrayList(allCommitables.values());
            // in case of 'base' commit, remove all 'deletions', mark all other items as non-modified, remove additions and copies from other urls.
            
            if (myIsDisableLocalModificationsCopying) {
                ArrayList harmlessItems = new ArrayList();
                for(int i = 0 ; i < commitItems.size(); i++) {
                    SVNCommitItem item = (SVNCommitItem) commitItems.get(i);
                    if (item.isDeleted()) {
                        // deletion or replacement, skip it.
                        continue;
                    }                
                    if (item.isAdded()) {
                        if (!item.isCopied()) {
                            // this is just new file or directory
                            continue;
                        }
                        SVNURL copyFromURL = item.getCopyFromURL();
                        if (copyFromURL == null) {
                            // also skip.
                            continue;
                        }                    
                        SVNEntry entry = wcAccess.getEntry(item.getFile(), false);
                        if (entry == null) {
                            continue;
                        }
                        SVNURL expectedURL = entry.getSVNURL();
                        if (!copyFromURL.equals(expectedURL)) {
                            // copied from some other location.
                            continue;
                        }
                    }
                    setCommitItemFlags(item, false, false);
                    harmlessItems.add(item);                
                }
                commitItems = harmlessItems;
            }
            
            // add parents to commits hash?
            if (makeParents) {
                for (int i = 0; i < newDirs.size(); i++) {
                    String newDirURL = (String) newDirs.get(i);
                    SVNURL url = SVNURL.parseURIEncoded(newDirURL);
                    SVNCommitItem item = new SVNCommitItem(null, url, null, SVNNodeKind.NONE, null, null, true, false, false, false, false, false);
                    commitItems.add(item);
                }
            }
            commitables = (SVNCommitItem[]) commitItems.toArray(new SVNCommitItem[commitItems.size()]);
            for (int i = 0; i < commitables.length; i++) {
                setCommitItemAccess(commitables[i], wcAccess);
            }
            allCommitables = new TreeMap();
            SVNURL url = SVNCommitUtil.translateCommitables(commitables, allCommitables);

            repos = createRepository(url, null, null, true);

            SVNCommitMediator mediator = new SVNCommitMediator(allCommitables);
            tmpFiles = mediator.getTmpFiles();

            message = SVNCommitUtil.validateCommitMessage(message);

            SVNURL rootURL = repos.getRepositoryRoot(true);
            SVNPropertiesManager.validateRevisionProperties(revprops);
            commitEditor = repos.getCommitEditor(message, null, true, revprops, mediator);
            info = SVNCommitter.commit(tmpFiles, allCommitables, rootURL, commitEditor, getEventDispatcher());
            commitEditor = null;

        } catch (SVNCancelException cancel) {
            throw cancel;
        } catch (SVNException e) {
            // wrap error message.
            SVNErrorMessage err = e.getErrorMessage().wrap("Commit failed (details follow):");
            SVNErrorManager.error(err, SVNLogType.WC);
        } finally {
            if (tmpFiles != null) {
                for (Iterator files = tmpFiles.iterator(); files.hasNext();) {
                    File file = (File) files.next();
                    SVNFileUtil.deleteFile(file);
                }
            }
            if (commitEditor != null && info == null) {
                // should we hide this exception?
                try {
                    commitEditor.abortEdit();
                } catch (SVNException e) {
                    SVNDebugLog.getDefaultLog().logFine(SVNLogType.WC, e);
                }
            }
            if (wcAccess != null) {
                wcAccess.close();
            }
        }
        if (info != null && info.getNewRevision() >= 0) {
            dispatchEvent(SVNEventFactory.createSVNEvent(null, SVNNodeKind.NONE, null, info.getNewRevision(), SVNEventAction.COMMIT_COMPLETED, null, null, null), ISVNEventHandler.UNKNOWN);
        }
        return info != null ? info : SVNCommitInfo.NULL;
    }

    private void copyReposToWC(List copyPairs, boolean makeParents) throws SVNException {
        for (Iterator pairs = copyPairs.iterator(); pairs.hasNext();) {
            CopyPair pair = (CopyPair) pairs.next();
            SVNRepositoryLocation[] locations = getLocations(SVNURL.parseURIEncoded(pair.mySource), null, null, pair.mySourcePegRevision, pair.mySourceRevision, SVNRevision.UNDEFINED);
            // new
            String actualURL = locations[0].getURL().toString();
            String originalSource = pair.mySource;
            pair.mySource = actualURL;
            pair.myOriginalSource = originalSource;
        }
        // get src and dst ancestors.
        String topDst = ((CopyPair) copyPairs.get(0)).myDst;
        if (copyPairs.size() > 1) {
            topDst = SVNPathUtil.removeTail(topDst);
        }
        String topSrc = ((CopyPair) copyPairs.get(0)).mySource;
        for(int i = 1; i < copyPairs.size(); i++) {
            CopyPair pair = (CopyPair) copyPairs.get(i);
            topSrc = SVNPathUtil.getCommonPathAncestor(topSrc, pair.mySource);
        }
        if (copyPairs.size() == 1) {
            topSrc = SVNPathUtil.removeTail(topSrc);
        }
        SVNRepository topSrcRepos = createRepository(SVNURL.parseURIEncoded(topSrc), null, null, false);
        try {
            for (Iterator pairs = copyPairs.iterator(); pairs.hasNext();) {
                CopyPair pair = (CopyPair) pairs.next();
                pair.mySourceRevisionNumber = getRevisionNumber(pair.mySourceRevision, topSrcRepos, null);
            }
            String reposPath = topSrcRepos.getLocation().toString();
            for (Iterator pairs = copyPairs.iterator(); pairs.hasNext();) {
                CopyPair pair = (CopyPair) pairs.next();
                String relativePath = SVNPathUtil.getPathAsChild(reposPath, pair.mySource);
                relativePath = SVNEncodingUtil.uriDecode(relativePath);
                SVNNodeKind kind = topSrcRepos.checkPath(relativePath, pair.mySourceRevisionNumber);
                if (kind == SVNNodeKind.NONE) {
                    SVNErrorMessage err;
                    if (pair.mySourceRevisionNumber >= 0) {
                        err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "Path ''{0}'' not found in revision {1}",
                                new Object[] {SVNURL.parseURIEncoded(pair.mySource), new Long(pair.mySourceRevisionNumber)});
                    } else {
                        err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "Path ''{0}'' not found in head revision",
                                SVNURL.parseURIEncoded(pair.mySource));
                    }
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                pair.mySourceKind = kind;
                SVNFileType dstType = SVNFileType.getType(new File(pair.myDst));
                if (dstType != SVNFileType.NONE) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "Path ''{0}'' already exists", new File(pair.myDst));
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                String dstParent = SVNPathUtil.removeTail(pair.myDst);
                SVNFileType dstParentFileType = SVNFileType.getType(new File(dstParent));
                if (makeParents && dstParentFileType == SVNFileType.NONE) {
                    // create parents.
                    addLocalParents(new File(dstParent), getEventDispatcher());
                } else if (dstParentFileType != SVNFileType.DIRECTORY) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY, "Path ''{0}'' is not a directory", dstParent);
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
            SVNWCAccess dstAccess = getWCAccess();
            try {
                probeOpen(dstAccess, new File(topDst), true, 0);
                for (Iterator pairs = copyPairs.iterator(); pairs.hasNext();) {
                    CopyPair pair = (CopyPair) pairs.next();
                    SVNEntry dstEntry = dstAccess.getEntry(new File(pair.myDst), true);
                    if (dstEntry != null) {
                        if (dstEntry.getDepth() == SVNDepth.EXCLUDE || dstEntry.isAbsent()) {
                            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                                    "''{0}'' is already under version control", new File(pair.myDst));
                            SVNErrorManager.error(err, SVNLogType.WC);
                        }
                        if (!dstEntry.isDirectory() && !dstEntry.isScheduledForDeletion() && !dstEntry.isDeleted()) {
                            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE,
                                    "Entry for ''{0}'' exists (though the working file is missing)", new File(pair.myDst));
                            SVNErrorManager.error(err, SVNLogType.WC);
                        }
                    }
                }
                String srcUUID = null;
                String dstUUID = null;
                try {
                    srcUUID = topSrcRepos.getRepositoryUUID(true);
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.RA_NO_REPOS_UUID) {
                        throw e;
                    }
                }
                String dstParent = topDst;
                if (copyPairs.size() == 1) {
                    dstParent = SVNPathUtil.removeTail(topDst);
                }
                try {
                    dstUUID = getUUIDFromPath(dstAccess, new File(dstParent));
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.RA_NO_REPOS_UUID) {
                        throw e;
                    }
                }
                boolean sameRepos = false;
                if (srcUUID != null) {
                    sameRepos = srcUUID.equals(dstUUID);
                }
                for (Iterator pairs = copyPairs.iterator(); pairs.hasNext();) {
                    CopyPair pair = (CopyPair) pairs.next();
                    copyReposToWC(pair, sameRepos, topSrcRepos, dstAccess);
                }
            } finally {
                    close(dstAccess);                    
            }
        } finally {
            topSrcRepos.closeSession();
        }

    }

    private void copyReposToWC(CopyPair pair, boolean sameRepositories, SVNRepository topSrcRepos, SVNWCAccess dstAccess) throws SVNException {
        long srcRevNum = pair.mySourceRevisionNumber;

        if (pair.mySourceKind == SVNNodeKind.DIR) {
            // do checkout
            String srcURL = pair.myOriginalSource;
            SVNURL url = SVNURL.parseURIEncoded(srcURL);
            SVNUpdateClient16 updateClient = new SVNUpdateClient16(getRepositoryPool(), getOptions());
            updateClient.setEventHandler(getEventDispatcher());

            File dstFile = new File(pair.myDst);
            SVNRevision srcRevision = pair.mySourceRevision;
            SVNRevision srcPegRevision = pair.mySourcePegRevision;
            updateClient.doCheckout(url, dstFile, srcPegRevision, srcRevision, SVNDepth.INFINITY, false);

            if (sameRepositories) {
                url = SVNURL.parseURIEncoded(pair.mySource);

                SVNAdminArea dstArea = dstAccess.open(dstFile, true, SVNWCAccess.INFINITE_DEPTH);
                SVNEntry dstRootEntry = dstArea.getEntry(dstArea.getThisDirName(), false);
                if (srcRevision == SVNRevision.HEAD) {
                    srcRevNum = dstRootEntry.getRevision();
                }
                SVNAdminArea dir = dstAccess.getAdminArea(dstFile.getParentFile());
                SVNWCManager.add(dstFile, dir, url, srcRevNum, SVNDepth.INFINITY);
                Map srcMergeInfo = calculateTargetMergeInfo(null, null, url, srcRevNum, topSrcRepos, false);
                extendWCMergeInfo(dstFile, dstRootEntry, srcMergeInfo, dstAccess);
            } else {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Source URL ''{0}'' is from foreign repository; leaving it as a disjoint WC", url);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        } else if (pair.mySourceKind == SVNNodeKind.FILE) {
            String srcURL = pair.mySource;
            SVNURL url = SVNURL.parseURIEncoded(srcURL);

            File dst = new File(pair.myDst);
            SVNAdminArea dir = dstAccess.getAdminArea(dst.getParentFile());
            File tmpFile = SVNAdminUtil.createTmpFile(dir);
            String path = getPathRelativeToRoot(null, url, null, null, topSrcRepos);
            SVNProperties props = new SVNProperties();
            OutputStream os = null;
            long revision = -1;
            try {
                os = SVNFileUtil.openFileForWriting(tmpFile);
                revision = topSrcRepos.getFile(path, srcRevNum, props, new SVNCancellableOutputStream(os, this));
                props = SVNMergeDriver.filterProperties(props, true, false, false, true);
            } finally {
                SVNFileUtil.closeFile(os);
            }
            if (srcRevNum < 0) {
                srcRevNum = revision;
            }
            SVNWCManager.addRepositoryFile(dir, dst.getName(), null, tmpFile, props, null,
                    sameRepositories ? pair.mySource : null,
                    sameRepositories ? srcRevNum : -1);

            SVNEntry entry = dstAccess.getEntry(dst, false);
            Map mergeInfo = calculateTargetMergeInfo(null, null, url, srcRevNum, topSrcRepos, false);
            extendWCMergeInfo(dst, entry, mergeInfo, dstAccess);

            SVNEvent event = SVNEventFactory.createSVNEvent(dst, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.COPY, null, null, null);
            dstAccess.handleEvent(event);

            sleepForTimeStamp();
        }
    }

    private void copyWCToWC(List copyPairs, boolean isMove, boolean makeParents) throws SVNException {
        for (Iterator pairs = copyPairs.iterator(); pairs.hasNext();) {
            CopyPair pair = (CopyPair) pairs.next();
            File source = new File(pair.mySource);
            SVNFileType srcFileType = SVNFileType.getType(source);
            if (srcFileType == SVNFileType.NONE) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.NODE_UNKNOWN_KIND,
                        "Path ''{0}'' does not exist", source);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            if (isMove && SVNWCUtil.isWorkingCopyRoot(source)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, 
                        "Cannot move ''{0}'' as it is the root of the working copy", source);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            SVNFileType dstFileType = SVNFileType.getType(new File(pair.myDst));
            if (dstFileType != SVNFileType.NONE) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                        "Path ''{0}'' already exists", new File(pair.myDst));
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            File dstParent = new File(SVNPathUtil.removeTail(pair.myDst));
            pair.myBaseName = SVNPathUtil.tail(pair.myDst);
            SVNFileType dstParentFileType = SVNFileType.getType(dstParent);
            if (makeParents && dstParentFileType == SVNFileType.NONE) {
                // create parents.
                addLocalParents(dstParent, getEventDispatcher());
            } else if (dstParentFileType != SVNFileType.DIRECTORY) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY,
                        "Path ''{0}'' is not a directory", dstParent);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        if (isMove) {
            moveWCToWC(copyPairs);
        } else {
            copyWCToWC(copyPairs);
        }
    }

    protected void copyDisjointWCToWC(File nestedWC) throws SVNException {
        SVNFileType nestedWCType = SVNFileType.getType(nestedWC);
        if (nestedWCType != SVNFileType.DIRECTORY) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                    "This kind of copy can be run on a root of a disjoint wc directory only");
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        nestedWC = new File(nestedWC.getAbsolutePath().replace(File.separatorChar, '/'));
        File nestedWCParent = nestedWC.getParentFile();
        if (nestedWCParent == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                    "{0} seems to be not a disjoint wc since it has no parent", nestedWC);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        SVNWCAccess parentWCAccess = createWCAccess();
        SVNWCAccess nestedWCAccess = createWCAccess();
        try {
            SVNAdminArea parentArea = parentWCAccess.open(nestedWCParent, true, 0);

            SVNEntry srcEntryInParent = parentWCAccess.getEntry(nestedWC, false);
            if (srcEntryInParent != null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                        "Entry ''{0}'' already exists in parent directory", nestedWC.getName());
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            SVNAdminArea nestedArea = nestedWCAccess.open(nestedWC, false, SVNWCAccess.INFINITE_DEPTH);

            SVNEntry nestedWCThisEntry = nestedWCAccess.getVersionedEntry(nestedWC, false);
            SVNEntry parentThisEntry = parentWCAccess.getVersionedEntry(nestedWCParent, false);

            // uuids may be identical while it might be absolutely independent repositories.
            // subversion uses repos roots comparison for local copies, and uuids comparison for
            // operations involving ra access. so, I believe we should act similarly here.

            if (nestedWCThisEntry.getRepositoryRoot() != null && parentThisEntry.getRepositoryRoot() != null &&
                    !nestedWCThisEntry.getRepositoryRoot().equals(parentThisEntry.getRepositoryRoot())) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                        "Cannot copy to ''{0}'', as it is not from repository ''{1}''; it is from ''{2}''",
                        new Object[] { nestedWCParent, nestedWCThisEntry.getRepositoryRootURL(),
                        parentThisEntry.getRepositoryRootURL() });
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            if (parentThisEntry.isScheduledForDeletion()) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                        "Cannot copy to ''{0}'', as it is scheduled for deletion", nestedWCParent);
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            if (nestedWCThisEntry.isScheduledForDeletion()) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                        "Cannot copy ''{0}'', as it is scheduled for deletion", nestedWC);
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            SVNURL nestedWCReposRoot = getReposRoot(nestedWC, null, SVNRevision.WORKING, nestedArea,
                    nestedWCAccess);
            String nestedWCPath = getPathRelativeToRoot(nestedWC, null, nestedWCReposRoot, nestedWCAccess, null);

            SVNURL parentReposRoot = getReposRoot(nestedWCParent, null, SVNRevision.WORKING, parentArea,
                    parentWCAccess);
            String parentPath = getPathRelativeToRoot(nestedWCParent, null, parentReposRoot, parentWCAccess, null);

            if (SVNPathUtil.isAncestor(nestedWCPath, parentPath)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                        "Cannot copy path ''{0}'' into its own child ''{1}",
                        new Object[] { nestedWCPath, parentPath });
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            if ((nestedWCThisEntry.isScheduledForAddition() && !nestedWCThisEntry.isCopied()) ||
                    nestedWCThisEntry.getURL() == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                        "Cannot copy or move ''{0}'': it is not in repository yet; " +
                        "try committing first", nestedWC);
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            copyDisjointDir(nestedWC, parentWCAccess, nestedWCParent);
            parentWCAccess.probeTry(nestedWC, true, SVNWCAccess.INFINITE_DEPTH);
        } finally {
            parentWCAccess.close();
            nestedWCAccess.close();
            sleepForTimeStamp();
        }
    }

    private void copyDisjointDir(File nestedWC, SVNWCAccess parentAccess, File nestedWCParent) throws SVNException {
        SVNWCClient16 wcClient = new SVNWCClient16((ISVNAuthenticationManager) null, null);
        wcClient.setEventHandler(getEventDispatcher());
        wcClient.doCleanup(nestedWC);

        SVNWCAccess nestedWCAccess = createWCAccess();
        SVNAdminArea dir;
        String copyFromURL = null;
        long copyFromRevision = -1;
        try {
            dir = nestedWCAccess.open(nestedWC, true, SVNWCAccess.INFINITE_DEPTH);
            SVNEntry nestedWCThisEntry = nestedWCAccess.getVersionedEntry(nestedWC, false);
            postCopyCleanup(dir);
            if (nestedWCThisEntry.isCopied()) {
                if (nestedWCThisEntry.getCopyFromURL() != null) {
                    copyFromURL = nestedWCThisEntry.getCopyFromURL();
                    copyFromRevision = nestedWCThisEntry.getCopyFromRevision();
                }

                Map attributes = new SVNHashMap();
                attributes.put(SVNProperty.URL, copyFromURL);
                dir.modifyEntry(dir.getThisDirName(), attributes, true, false);
            } else {
                copyFromURL = nestedWCThisEntry.getURL();
                copyFromRevision = nestedWCThisEntry.getRevision();
            }
        } finally {
            nestedWCAccess.close();
        }
        SVNWCManager.add(nestedWC, parentAccess.getAdminArea(nestedWCParent),
                SVNURL.parseURIEncoded(copyFromURL), copyFromRevision, SVNDepth.INFINITY);
    }

    private void copyWCToWC(List pairs) throws SVNException {
        // find common ancestor for all dsts.
        String dstParentPath = null;
        for (Iterator ps = pairs.iterator(); ps.hasNext();) {
            CopyPair pair = (CopyPair) ps.next();
            String dstPath = pair.myDst;
            if (dstParentPath == null) {
                dstParentPath = SVNPathUtil.removeTail(pair.myDst);
            }
            dstParentPath = SVNPathUtil.getCommonPathAncestor(dstParentPath, dstPath);
        }
        SVNWCAccess dstAccess = getWCAccess();
        try {
            open(dstAccess, new File(dstParentPath), true, false, 0);
            for (Iterator ps = pairs.iterator(); ps.hasNext();) {
                CopyPair pair = (CopyPair) ps.next();
                checkCancelled();
                SVNWCAccess srcAccess = null;
                try {
                    // do real copy.
                    File sourceFile = new File(pair.mySource);
                    copyFiles(sourceFile, new File(dstParentPath), dstAccess, pair.myBaseName, false);
                } finally {
                    if (srcAccess != null && srcAccess != dstAccess) {
                        close(srcAccess);
                    }
                }
            }
        } finally {
            close(dstAccess);
            sleepForTimeStamp();
        }
    }

    private void moveWCToWC(List pairs) throws SVNException {
        for (Iterator ps = pairs.iterator(); ps.hasNext();) {
            CopyPair pair = (CopyPair) ps.next();
            checkCancelled();
            File srcParent = new File(SVNPathUtil.removeTail(pair.mySource));
            File dstParent = new File(SVNPathUtil.removeTail(pair.myDst));
            File sourceFile = new File(pair.mySource);
            SVNFileType srcType = SVNFileType.getType(sourceFile);
            SVNWCAccess srcAccess = createWCAccess();
            SVNWCAccess dstAccess = null;
            try {
                srcAccess.open(srcParent, true, srcType == SVNFileType.DIRECTORY ? -1 : 0);
                if (srcParent.equals(dstParent)) {
                    dstAccess = srcAccess;
                } else {
                    String srcParentPath = srcParent.getAbsolutePath().replace(File.separatorChar, '/');
                    srcParentPath = SVNPathUtil.validateFilePath(srcParentPath);
                    String dstParentPath = dstParent.getAbsolutePath().replace(File.separatorChar, '/');
                    dstParentPath = SVNPathUtil.validateFilePath(dstParentPath);
                    if (srcType == SVNFileType.DIRECTORY &&
                            SVNPathUtil.isAncestor(srcParentPath, dstParentPath)) {
                        dstAccess = srcAccess;
                        if (dstAccess.getAdminArea(dstParent) == null) {
                            dstAccess.open(dstParent, true, 0);
                        }
                    } else {
                        dstAccess = createWCAccess();
                        dstAccess.open(dstParent, true, 0);
                    }
                }
                copyFiles(sourceFile, dstParent, dstAccess, pair.myBaseName, true);
                // delete src.
                SVNWCManager.delete(srcAccess, srcAccess.getAdminArea(srcParent), sourceFile, true, true);
            } finally {
                if (dstAccess != null && dstAccess != srcAccess) {
                    dstAccess.close();
                } 
                srcAccess.close();
            }
        }
        sleepForTimeStamp();
    }

    private void copyFiles(File src, File dstParent, SVNWCAccess dstAccess, String dstName, boolean move) throws SVNException {
        SVNWCAccess srcAccess = getWCAccess();
        try {
            probeOpen(srcAccess, src, false, -1);
            SVNEntry dstEntry = dstAccess.getVersionedEntry(dstParent, false);
            SVNEntry srcEntry = srcAccess.getVersionedEntry(src, false);

            if (srcEntry.getRepositoryRoot() != null && dstEntry.getRepositoryRoot() != null &&
                    !srcEntry.getRepositoryRoot().equals(dstEntry.getRepositoryRoot())) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                        "Cannot copy to ''{0}'', as it is not from repository ''{1}''; it is from ''{2}''",
                        new Object[] {dstParent, srcEntry.getRepositoryRootURL(), dstEntry.getRepositoryRootURL()});
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            if (dstEntry.isScheduledForDeletion()) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                        "Cannot copy to ''{0}'', as it is scheduled for deletion", dstParent);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            SVNFileType srcType = SVNFileType.getType(src);
            if (srcType == SVNFileType.FILE || srcType == SVNFileType.SYMLINK) {
                if (srcEntry.isScheduledForAddition() && !(srcEntry.isCopied() && srcEntry.getCopyFromURL() != null)) {
                    copyAddedFileAdm(src, srcAccess, dstAccess, dstParent, dstName, true);
                } else {
                    copyFileAdm(src, srcAccess, dstParent, dstAccess, dstName, move);
                }
            } else if (srcType == SVNFileType.DIRECTORY) {
                if (srcEntry.isScheduledForAddition() && !(srcEntry.isCopied() && srcEntry.getCopyFromURL() != null)) {
                    copyAddedDirAdm(src, srcAccess, dstParent, dstAccess, dstName, true);
                } else {
                    copyDirAdm(src, srcAccess, dstAccess, dstParent, dstName);
                }
            }
        } finally {
            close(srcAccess);
        }
    }

    private void copyFileAdm(File src, SVNWCAccess srcAccess, File dstParent, SVNWCAccess dstAccess, String dstName, boolean move) throws SVNException {
        File dst = new File(dstParent, dstName);
        SVNFileType dstType = SVNFileType.getType(dst);
        if (dstType != SVNFileType.NONE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "''{0}'' already exists and is in the way", dst);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        SVNEntry dstEntry = dstAccess.getEntry(dst, false);
        if (dstEntry != null && !dstEntry.isScheduledForDeletion()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "There is already a versioned item ''{0}''", dst);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        SVNEntry srcEntry = srcAccess.getVersionedEntry(src, false);
        if ((srcEntry.isScheduledForAddition() && !srcEntry.isCopied()) || srcEntry.getURL() == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "Cannot copy or move ''{0}'': it is not in repository yet; " +
            		"try committing first", src);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        String copyFromURL;
        long copyFromRevision;
        SVNAdminArea srcDir = srcAccess.getAdminArea(src.getParentFile());
        if (srcEntry.isCopied()) {
            // get cf info - one of the parents has to keep that.
            SVNLocationEntry location = determineCopyFromInfo(src, srcAccess, srcEntry, dstEntry);
            copyFromURL = location.getPath();
            copyFromRevision = location.getRevision();
        } else {
            copyFromURL = srcEntry.getURL();
            copyFromRevision = srcEntry.getRevision();
        }
        String changelistName = srcEntry.getChangelistName();
        // copy base file.
        File srcBaseFile = new File(src.getParentFile(), SVNAdminUtil.getTextBasePath(src.getName(), false));
        File dstBaseFile = new File(dstParent, SVNAdminUtil.getTextBasePath(dstName, true));
        SVNFileUtil.copyFile(srcBaseFile, dstBaseFile, false);
        SVNVersionedProperties srcBaseProps = srcDir.getBaseProperties(src.getName());
        SVNVersionedProperties srcWorkingProps = srcDir.getProperties(src.getName());

        SVNAdminArea dstDir = dstAccess.getAdminArea(dstParent);
        if (srcWorkingProps.getPropertyValue(SVNProperty.SPECIAL) != null) {
            SVNWCManager.addRepositoryFile(dstDir, dstName, null, srcBaseFile, srcBaseProps.asMap(), srcWorkingProps.asMap(), copyFromURL, copyFromRevision);

            String sourceLinkTarget = SVNFileUtil.getSymlinkName(src);
            String targetLinkTarget = SVNFileUtil.getSymlinkName(dst);
            if (sourceLinkTarget != null && !sourceLinkTarget.equals(targetLinkTarget)) {
                SVNFileUtil.deleteFile(dst);
                SVNFileUtil.createSymlink(dst, sourceLinkTarget);
            }
        } else {
            File tmpWCFile = SVNAdminUtil.createTmpFile(dstDir);
            SVNFileUtil.copyFile(src, tmpWCFile, false);
            SVNWCManager.addRepositoryFile(dstDir, dstName, tmpWCFile, null, srcBaseProps.asMap(), srcWorkingProps.asMap(), copyFromURL, copyFromRevision);
        }
        
        if (changelistName != null && move) {
            dstEntry = dstAccess.getEntry(dst, false);
            if (dstEntry != null) {
                SVNLog log = dstDir.getLog();
                SVNProperties command = new SVNProperties();
                command.put(SVNProperty.shortPropertyName(SVNProperty.CHANGELIST), changelistName);
                log.logChangedEntryProperties(dstName, command);
                log.save();
                dstDir.runLogs();
            }
        }
        SVNEvent event = SVNEventFactory.createSVNEvent(dst, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.COPY, null, null, null);
        dstAccess.handleEvent(event);
    }

    private void copyAddedFileAdm(File src, SVNWCAccess srcAccess, SVNWCAccess dstAccess, File dstParent, String dstName, boolean isAdded) throws SVNException {
        File dst = new File(dstParent, dstName);
        SVNFileType srcType = SVNFileType.getType(src);
        if (srcType == SVNFileType.SYMLINK) {
            String linkName = SVNFileUtil.getSymlinkName(src);
            SVNFileUtil.createSymlink(dst, linkName);
        } else {
            SVNFileUtil.copyFile(src, dst, false);
        }
        if (isAdded) {
            SVNWCManager.add(dst, dstAccess.getAdminArea(dstParent), null, SVNRepository.INVALID_REVISION, SVNDepth.INFINITY);
            copyProps(src, dst, srcAccess, dstAccess);
        }
    }

    private void copyDirAdm(File src, SVNWCAccess srcAccess, SVNWCAccess dstAccess, File dstParent,
            String dstName) throws SVNException {
        File dst = new File(dstParent, dstName);
        SVNEntry srcEntry = srcAccess.getVersionedEntry(src, false);
        if ((srcEntry.isScheduledForAddition() && !srcEntry.isCopied()) || srcEntry.getURL() == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                    "Cannot copy or move ''{0}'': it is not in repository yet; " +
                    "try committing first", src);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        SVNFileUtil.copyDirectory(src, dst, true, getEventDispatcher());
        SVNWCClient16 wcClient = new SVNWCClient16((ISVNAuthenticationManager) null, null);
        wcClient.setEventHandler(getEventDispatcher());
        wcClient.doCleanup(dst);

        SVNWCAccess tgtAccess = getWCAccess();
        SVNAdminArea dir;
        String copyFromURL = null;
        long copyFromRevision = -1;
        try {
            dir = open(tgtAccess, dst, true, false, -1);
            postCopyCleanup(dir);
            if (srcEntry.isCopied()) {
                SVNEntry dstEntry = dstAccess.getEntry(dst, false);
                SVNLocationEntry info = determineCopyFromInfo(src, srcAccess, srcEntry, dstEntry);
                copyFromURL = info.getPath();
                copyFromRevision = info.getRevision();

                Map attributes = new SVNHashMap();
                attributes.put(SVNProperty.URL, copyFromURL);
                dir.modifyEntry(dir.getThisDirName(), attributes, true, false);
            } else {
                copyFromURL = srcEntry.getURL();
                copyFromRevision = srcEntry.getRevision();
            }
        } finally {
            close(tgtAccess);
        }
        SVNWCManager.add(dst, dstAccess.getAdminArea(dstParent), SVNURL.parseURIEncoded(copyFromURL), copyFromRevision, SVNDepth.INFINITY);
    }

    private void copyAddedDirAdm(File src, SVNWCAccess srcAccess, File dstParent, SVNWCAccess dstParentAccess, String dstName, boolean isAdded) throws SVNException {
        File dst = new File(dstParent, dstName);
        if (!isAdded) {
            SVNFileUtil.copyDirectory(src, dst, true, getEventDispatcher());
        } else {
            checkCancelled();
            dst.mkdirs();

            SVNWCManager.add(dst, dstParentAccess.getAdminArea(dstParent), null, SVNRepository.INVALID_REVISION, SVNDepth.INFINITY);
            copyProps(src, dst, srcAccess, dstParentAccess);
            
            SVNAdminArea srcChildArea = srcAccess.retrieve(src);

            File[] entries = SVNFileListUtil.listFiles(src);

            for (int i = 0; entries != null && i < entries.length; i++) {
                checkCancelled();
                File fsEntry = entries[i];
                String name = fsEntry.getName();
                if (SVNFileUtil.getAdminDirectoryName().equals(name)) {
                    continue;
                }

                SVNEntry entry = srcChildArea.getEntry(name, true);
                if (fsEntry.isDirectory()) {
                    copyAddedDirAdm(fsEntry, srcAccess, dst, dstParentAccess, name, entry != null);
                } else if (fsEntry.isFile()) {
                    copyAddedFileAdm(fsEntry, srcAccess, dstParentAccess, dst, name, entry != null);
                }
            }
        }
    }

    private void copyProps(File src, File dst, SVNWCAccess srcAccess, SVNWCAccess dstAccess) throws SVNException {
        SVNEntry srcEntry = srcAccess.getVersionedEntry(src, false);
        SVNAdminArea srcArea = srcEntry.getAdminArea();
        SVNVersionedProperties srcProps = srcArea.getProperties(srcEntry.getName());
        Collection propNames = srcProps.getPropertyNames(null);
        for (Iterator propNamesIter = propNames.iterator(); propNamesIter.hasNext();) {
            String propName = (String) propNamesIter.next();
            SVNPropertyValue propValue = srcProps.getPropertyValue(propName);
            SVNPropertiesManager.setProperty(dstAccess, dst, propName, propValue, false);
        }
    }
    
    private SVNLocationEntry determineCopyFromInfo(File src, SVNWCAccess srcAccess, SVNEntry srcEntry, SVNEntry dstEntry) throws SVNException {
        String url;
        long rev;
        if (srcEntry.getCopyFromURL() != null) {
            url = srcEntry.getCopyFromURL();
            rev = srcEntry.getCopyFromRevision();
        } else {
            SVNLocationEntry info = getCopyFromInfoFromParent(src, srcAccess);
            url = info.getPath();
            rev = info.getRevision();
        }
        if (dstEntry != null && rev == dstEntry.getRevision() && url.equals(dstEntry.getCopyFromURL())) {
            url = null;
            rev = -1;
        }
        return new SVNLocationEntry(rev, url);
    }

    private SVNLocationEntry getCopyFromInfoFromParent(File file, SVNWCAccess access) throws SVNException {
        File parent = file.getParentFile();
        String rest = file.getName();
        String url = null;
        long rev = -1;
        while (parent != null && url == null) {
            try {
                SVNEntry entry = access.getVersionedEntry(parent, false);
                url = entry.getCopyFromURL();
                rev = entry.getCopyFromRevision();
            } catch (SVNException e) {
                SVNWCAccess wcAccess = SVNWCAccess.newInstance(null);
                try {
                    probeOpen(wcAccess, parent, false, -1);
                    SVNEntry entry = wcAccess.getVersionedEntry(parent, false);
                    url = entry.getCopyFromURL();
                    rev = entry.getCopyFromRevision();
                } finally {
                    close(wcAccess);
                }
            }
            if (url != null) {
                url = SVNPathUtil.append(url, SVNEncodingUtil.uriEncode(rest));
            } else {
                rest = SVNPathUtil.append(parent.getName(), rest);
                parent = parent.getParentFile();
            }
        }
        return new SVNLocationEntry(rev, url);
    }


    private void addLocalParents(File path, ISVNEventHandler handler) throws SVNException {
        boolean created = path.mkdirs();
        SVNWCClient16 wcClient = new SVNWCClient16((ISVNAuthenticationManager) null, null);
        try {
            wcClient.setEventHandler(handler);
            wcClient.doAdd(path, false, false, true, SVNDepth.EMPTY, true, true);
        } catch (SVNException e) {
            if (created) {
                SVNFileUtil.deleteAll(path, true);
            }
            throw e;
        }
    }

    private void extendWCMergeInfo(File path, SVNEntry entry, Map mergeInfo, SVNWCAccess access) throws SVNException {
        Map wcMergeInfo = SVNPropertiesManager.parseMergeInfo(path, entry, false);
        if (wcMergeInfo != null && mergeInfo != null) {
            wcMergeInfo = SVNMergeInfoUtil.mergeMergeInfos(wcMergeInfo, mergeInfo);
        } else if (wcMergeInfo == null) {
            wcMergeInfo = mergeInfo;
        }
        SVNPropertiesManager.recordWCMergeInfo(path, wcMergeInfo, access);
    }

    private Map calculateTargetMergeInfo(File srcFile, SVNWCAccess access, SVNURL srcURL, long srcRevision,
            SVNRepository repository, boolean noReposAccess) throws SVNException {
        boolean isLocallyAdded = false;
        SVNEntry entry = null;
        SVNURL url = null;
        if (access != null) {
            entry = access.getVersionedEntry(srcFile, false);
            if (entry.isScheduledForAddition() && !entry.isCopied()) {
                isLocallyAdded = true;
            } else {
                if (entry.getCopyFromURL() != null) {
                    url = entry.getCopyFromSVNURL();
                    srcRevision = entry.getCopyFromRevision();
                } else if (entry.getURL() != null) {
                    url = entry.getSVNURL();
                    srcRevision = entry.getRevision();
                } else {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL,
                            "Entry for ''{0}'' has no URL", srcFile);
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
        } else {
            url = srcURL;
        }

        Map targetMergeInfo = null;
        if (!isLocallyAdded) {
            String mergeInfoPath;
            if (!noReposAccess) {
                SVNRepository repos = repository;
                if (repos == null) {
                    repos = createRepository(url, null, false);
                }
                SVNURL oldLocation = null;
                try {
                    mergeInfoPath = getPathRelativeToSession(url, null, repos);
                    if (mergeInfoPath == null) {
                        oldLocation = repos.getLocation();
                        repos.setLocation(url, false);
                        mergeInfoPath = "";
                    }
                    targetMergeInfo = getReposMergeInfo(repos, mergeInfoPath, srcRevision,
                    		SVNMergeInfoInheritance.INHERITED, true);
                } finally {
                    if (repository == null) {
                        repos.closeSession();
                    } else if (oldLocation != null) {
                        repos.setLocation(oldLocation, false);
                    }
                }
            } else {
                targetMergeInfo = getWCMergeInfo(srcFile, entry, null, SVNMergeInfoInheritance.INHERITED, false,
                        new boolean[1]);
            }
        }
        return targetMergeInfo;
    }

    private static class CopyCommitPathHandler implements ISVNCommitPathHandler {

        private Map myPathInfos;
        private boolean myIsMove;

        public CopyCommitPathHandler(Map pathInfos, boolean isMove) {
            myPathInfos = pathInfos;
            myIsMove = isMove;
        }

        public boolean handleCommitPath(String commitPath, ISVNEditor commitEditor) throws SVNException {
            CopyPathInfo pathInfo = (CopyPathInfo) myPathInfos.get(commitPath);
            boolean doAdd = false;
            boolean doDelete = false;
            if (pathInfo.isDirAdded) {
                commitEditor.addDir(commitPath, null, SVNRepository.INVALID_REVISION);
                return true;
            }

            if (pathInfo.isResurrection) {
                if (!myIsMove) {
                    doAdd = true;
                }
            } else {
                if (myIsMove) {
                    if (commitPath.equals(pathInfo.mySourceRelativePath)) {
                        doDelete = true;
                    } else {
                        doAdd = true;
                    }
                } else {
                    doAdd = !pathInfo.myIsOnlyPinExternals;
                }
            }
            if (doDelete) {
                commitEditor.deleteEntry(commitPath, -1);
            }
            boolean closeDir = false;
            if (doAdd) {
                SVNPathUtil.checkPathIsValid(commitPath);
                if (pathInfo.mySourceKind == SVNNodeKind.DIR) {
                    commitEditor.addDir(commitPath, pathInfo.mySourcePath, pathInfo.mySourceRevisionNumber);
                    if (pathInfo.myMergeInfoProp != null) {
                        commitEditor.changeDirProperty(SVNProperty.MERGE_INFO, SVNPropertyValue.create(pathInfo.myMergeInfoProp));
                    }
                    closeDir = true;
                } else {
                    commitEditor.addFile(commitPath, pathInfo.mySourcePath, pathInfo.mySourceRevisionNumber);
                    if (pathInfo.myMergeInfoProp != null) {
                        commitEditor.changeFileProperty(commitPath, SVNProperty.MERGE_INFO, SVNPropertyValue.create(pathInfo.myMergeInfoProp));
                    }
                    commitEditor.closeFile(commitPath, null);
                }
            }
            if (pathInfo.myExternalsProp != null) {
                if (!closeDir) {
                    commitEditor.openDir(commitPath, SVNRepository.INVALID_REVISION);
                    closeDir = true;
                }
                commitEditor.changeDirProperty(SVNProperty.EXTERNALS, SVNPropertyValue.create(pathInfo.myExternalsProp));
            }

            return closeDir;
        }
    }

    private static class CopyPathInfo {
        public String mySourceRelativePath;
        public boolean isDirAdded;
        public boolean isResurrection;

        public SVNNodeKind mySourceKind;

        public String mySource;
        public String mySourcePath;
        public String myDstPath;

        public String myMergeInfoProp;
        public long mySourceRevisionNumber;
        public String myExternalsProp;
        public boolean myIsOnlyPinExternals;
    }

    private static class CopyPair {

        public String mySource;
        public String myOriginalSource;

        public SVNNodeKind mySourceKind;

        public SVNRevision mySourceRevision;
        public SVNRevision mySourcePegRevision;
        public long mySourceRevisionNumber;

        public String myBaseName;
        public String myDst;

        public void setSourceRevisions(SVNRevision pegRevision, SVNRevision revision) {
            if (pegRevision == SVNRevision.UNDEFINED) {
                if (SVNPathUtil.isURL(mySource)) {
                    pegRevision = SVNRevision.HEAD;
                } else {
                    pegRevision = SVNRevision.WORKING;
                }
            }
            if (revision == SVNRevision.UNDEFINED) {
                revision = pegRevision;
            }
            mySourceRevision = revision;
            mySourcePegRevision = pegRevision;
        }
    }

}