package org.tmatesoft.svn.core.internal.wc2.ng; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.tmatesoft.svn.core.SVNCommitInfo; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNMergeInfoInheritance; import org.tmatesoft.svn.core.SVNMergeRangeList; import org.tmatesoft.svn.core.SVNNodeKind; import org.tmatesoft.svn.core.SVNProperties; import org.tmatesoft.svn.core.SVNProperty; import org.tmatesoft.svn.core.SVNPropertyValue; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.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.SVNCommitUtil; import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; import org.tmatesoft.svn.core.internal.wc.SVNExternal; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.core.internal.wc17.SVNCommitter17; import org.tmatesoft.svn.core.internal.wc17.SVNWCContext; import org.tmatesoft.svn.core.internal.wc17.SVNWCUtils; import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb; import org.tmatesoft.svn.core.internal.wc17.db.Structure; import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.NodeOriginInfo; import org.tmatesoft.svn.core.internal.wc2.SvnWcGeneration; import org.tmatesoft.svn.core.internal.wc2.ng.SvnNgCommitUtil.ISvnUrlKindCallback; import org.tmatesoft.svn.core.io.ISVNEditor; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.wc.SVNRevision; import org.tmatesoft.svn.core.wc2.SvnCommitItem; import org.tmatesoft.svn.core.wc2.SvnCommitPacket; import org.tmatesoft.svn.core.wc2.SvnCopySource; import org.tmatesoft.svn.core.wc2.SvnRemoteCopy; import org.tmatesoft.svn.core.wc2.hooks.ISvnCommitHandler; import org.tmatesoft.svn.util.SVNLogType; public class SvnNgWcToReposCopy extends SvnNgOperationRunner<SVNCommitInfo, SvnRemoteCopy> implements ISvnUrlKindCallback { @Override public boolean isApplicable(SvnRemoteCopy operation, SvnWcGeneration wcGeneration) throws SVNException { return areAllSourcesLocal(operation) && !operation.getFirstTarget().isLocal(); } private boolean areAllSourcesLocal(SvnRemoteCopy operation) { // need all sources to be wc files at WORKING. // BASE revision meas repos_to_repos copy for(SvnCopySource source : operation.getSources()) { if (source.getSource().isFile() && (source.getRevision() == SVNRevision.WORKING || source.getRevision() == SVNRevision.UNDEFINED)) { continue; } return false; } return true; } @Override protected SVNCommitInfo run(SVNWCContext context) throws SVNException { SVNCommitInfo info = null; try { info = doRun(context, getOperation().getFirstTarget().getURL()); } catch (SVNException e) { SVNErrorCode code = e.getErrorMessage().getErrorCode(); if (!getOperation().isFailWhenDstExists() && getOperation().getSources().size() == 1 && (code == SVNErrorCode.ENTRY_EXISTS || code == SVNErrorCode.FS_ALREADY_EXISTS)) { SvnCopySource source = getOperation().getSources().iterator().next(); SVNURL target = getOperation().getFirstTarget().getURL(); target = target.appendPath(source.getSource().getFile().getName(), false); info = doRun(context, target); } else { throw e; } } if (info != null) { getOperation().receive(getOperation().getFirstTarget(), info); } return info; } protected SVNCommitInfo doRun(SVNWCContext context, SVNURL target) throws SVNException { if (getOperation().isMove()) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Moves between the working copy and the repository are not supported"); SVNErrorManager.error(err, SVNLogType.WC); } Collection<SvnCopySource> sources = getOperation().getSources(); Collection<SvnCopyPair> copyPairs = new ArrayList<SvnNgWcToReposCopy.SvnCopyPair>(); if (sources.size() > 1) { for (SvnCopySource copySource : sources) { SvnCopyPair copyPair = new SvnCopyPair(); String baseName; copyPair.source = copySource.getSource().getFile(); baseName = copyPair.source.getName(); copyPair.dst = target; copyPair.dst = copyPair.dst.appendPath(baseName, false); copyPairs.add(copyPair); } } else if (sources.size() == 1) { SvnCopyPair copyPair = new SvnCopyPair(); SvnCopySource source = sources.iterator().next(); copyPair.source= source.getSource().getFile(); copyPair.dst = target; copyPairs.add(copyPair); } return copy(copyPairs, getOperation().isMakeParents(), getOperation().getRevisionProperties(), getOperation().getCommitMessage(), getOperation().getCommitHandler()); } private SVNCommitInfo copy(Collection<SvnCopyPair> copyPairs, boolean makeParents, SVNProperties revisionProperties, String commitMessage, ISvnCommitHandler commitHandler) throws SVNException { SvnCopyPair firstPair = copyPairs.iterator().next(); SVNURL topDstUrl = firstPair.dst.removePathTail(); for (SvnCopyPair pair : copyPairs) { topDstUrl = SVNURLUtil.getCommonURLAncestor(topDstUrl, pair.dst); } File topSrcPath = getCommonCopyAncestor(copyPairs); SVNRepository repository = getRepositoryAccess().createRepository(topDstUrl, topSrcPath); topDstUrl = repository.getLocation(); Collection<SVNURL> parents = null; if (makeParents) { parents = findMissingParents(topDstUrl, repository); } for (SvnCopyPair pair : copyPairs) { String path = SVNURLUtil.getRelativeURL(repository.getLocation(), pair.dst, false); if (repository.checkPath(path, -1) != SVNNodeKind.NONE) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_ALREADY_EXISTS, "Path ''{0}'' already exists", pair.dst); SVNErrorManager.error(err, SVNLogType.WC); } } SvnCommitItem[] items = new SvnCommitItem[(parents != null ? parents.size() : 0) + copyPairs.size()]; int index = 0; if (makeParents && parents != null) { for (SVNURL parent : parents) { SvnCommitItem parentItem = new SvnCommitItem(); parentItem.setUrl(parent); parentItem.setFlags(SvnCommitItem.ADD); parentItem.setKind(SVNNodeKind.DIR); items[index++] = parentItem; } } for (SvnCopyPair svnCopyPair : copyPairs) { SvnCommitItem item = new SvnCommitItem(); item.setUrl(svnCopyPair.dst); item.setPath(svnCopyPair.source); item.setFlags(SvnCommitItem.ADD); item.setKind(SVNNodeKind.DIR); items[index++] = item; } commitMessage = getOperation().getCommitHandler().getCommitMessage(commitMessage, items); if (commitMessage == null) { return SVNCommitInfo.NULL; } commitMessage = SVNCommitUtil.validateCommitMessage(commitMessage); revisionProperties = getOperation().getCommitHandler().getRevisionProperties(commitMessage, items, revisionProperties); if (revisionProperties == null) { return SVNCommitInfo.NULL; } SvnCommitPacket packet = new SvnCommitPacket(); SVNURL repositoryRoot = repository.getRepositoryRoot(true); if (parents != null) { for (SVNURL parent : parents) { String parentPath = SVNURLUtil.getRelativeURL(repositoryRoot, parent, false); packet.addItem(null, SVNNodeKind.DIR, repositoryRoot, parentPath, -1, null, -1, null, SvnCommitItem.ADD); } } for (SvnCopyPair svnCopyPair : copyPairs) { Map<File, String> externals = getOperation().getExternalsHandler() != null ? new HashMap<File, String>() : null; SvnNgCommitUtil.harvestCopyCommitables(getWcContext(), svnCopyPair.source, svnCopyPair.dst, packet, this, getOperation().getCommitParameters(), externals); SvnCommitItem item = packet.getItem(svnCopyPair.source); if (item == null) { continue; } Map<String, SVNMergeRangeList> mergeInfo = calculateTargetMergeInfo(svnCopyPair.source, -1, repository); String mergeInfoProperty = getWcContext().getProperty(svnCopyPair.source, SVNProperty.MERGE_INFO); Map<String, SVNMergeRangeList> wcMergeInfo = mergeInfoProperty != null ? SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(mergeInfoProperty), null) : null; if (wcMergeInfo != null && mergeInfo != null) { mergeInfo = SVNMergeInfoUtil.mergeMergeInfos(mergeInfo, wcMergeInfo); } else if (mergeInfo == null) { mergeInfo = wcMergeInfo; } String extendedMergeInfoValue = null; if (wcMergeInfo != null) { extendedMergeInfoValue = SVNMergeInfoUtil.formatMergeInfoToString(wcMergeInfo, null); item.addOutgoingProperty(SVNProperty.MERGE_INFO, SVNPropertyValue.create(extendedMergeInfoValue)); } // append externals changes if (externals != null && !externals.isEmpty()) { includeExternalsChanges(repository, packet, externals, svnCopyPair); } } if (getOperation().isDisableLocalModifications()) { SvnCommitPacket oldPacket = packet; packet = filterLocalModifications(packet); if (packet.isEmpty()) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, buildErrorMessageWithDebugInformation(oldPacket)); SVNErrorManager.error(err, SVNLogType.DEFAULT); } } Map<String, SvnCommitItem> committables = new TreeMap<String, SvnCommitItem>(); SVNURL url = SvnNgCommitUtil.translateCommitables(packet.getItems(packet.getRepositoryRoots().iterator().next()), committables); repository.setLocation(url, false); ISVNEditor commitEditor = repository.getCommitEditor(commitMessage, null, false, revisionProperties, null); SVNCommitter17 committer = new SVNCommitter17(getWcContext(), committables, repositoryRoot, null, null, null); SVNCommitUtil.driveCommitEditor(committer, committables.keySet(), commitEditor, -1); committer.sendTextDeltas(commitEditor); SVNCommitInfo info = commitEditor.closeEdit(); deleteDeleteFiles(committer, getOperation().getCommitParameters()); return info; } private String buildErrorMessageWithDebugInformation(SvnCommitPacket oldPacket) { StringBuilder stringBuilder = new StringBuilder("Unable to perform wc to remote copy without local modifications:").append('\n'); stringBuilder.append("Commit packet was:").append('\n'); final Collection<SVNURL> repositoryRoots = oldPacket.getRepositoryRoots(); for (SVNURL oldRoot : repositoryRoots) { stringBuilder.append(oldRoot).append(" :").append('\n'); Collection<SvnCommitItem> oldItems = oldPacket.getItems(oldRoot); if (oldItems != null) { for (SvnCommitItem oldItem : oldItems) { stringBuilder.append("path=").append(oldItem.getPath()).append('\n'); stringBuilder.append("kind=").append(oldItem.getKind()).append('\n'); stringBuilder.append("url=").append(oldItem.getUrl()).append('\n'); stringBuilder.append("revision=").append(oldItem.getRevision()).append('\n'); stringBuilder.append("copyUrl=").append(oldItem.getCopyFromUrl()).append('\n'); stringBuilder.append("copyRevision=").append(oldItem.getCopyFromRevision()).append('\n'); stringBuilder.append("flags=").append(oldItem.getFlags()).append('\n'); } } } return stringBuilder.toString(); } private SvnCommitPacket filterLocalModifications(SvnCommitPacket packet) throws SVNException { final SvnCommitPacket filteredPacket = new SvnCommitPacket(); filteredPacket.setLockTokens(packet.getLockTokens()); filteredPacket.setLockingContext(packet.getRunner(), packet.getLockingContext()); final Collection<SVNURL> repositoryRoots = packet.getRepositoryRoots(); for (SVNURL repositoryRoot : repositoryRoots) { final Collection<SvnCommitItem> items = packet.getItems(repositoryRoot); for (SvnCommitItem item : items) { if (item.hasFlag(SvnCommitItem.DELETE)) { continue; } if (item.hasFlag(SvnCommitItem.ADD)) { if (!item.hasFlag(SvnCommitItem.COPY)) { continue; } final SVNURL copyFromUrl = item.getCopyFromUrl(); if (copyFromUrl == null) { continue; } final ISVNWCDb.WCDbBaseInfo baseInfo; try { baseInfo = getWcContext().getDb().getBaseInfo(item.getPath(), ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRootUrl, ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRelPath); } catch (SVNException e) { if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) { throw e; } else { continue; } } final SVNURL url = baseInfo.reposRootUrl.appendPath(SVNFileUtil.getFilePath(baseInfo.reposRelPath), false); if (!copyFromUrl.equals(url)) { continue; } } item.setFlags(item.getFlags() & (~SvnCommitItem.TEXT_MODIFIED) & (~SvnCommitItem.PROPS_MODIFIED)); filteredPacket.addItem(item, repositoryRoot); } } return filteredPacket; } private void includeExternalsChanges(SVNRepository repos, SvnCommitPacket packet, Map<File, String> externalsStorage, SvnCopyPair svnCopyPair) throws SVNException { for (File externalHolder : externalsStorage.keySet()) { String externalsPropString = (String) externalsStorage.get(externalHolder); SVNExternal[] externals = SVNExternal.parseExternals(externalHolder.getAbsolutePath(), externalsPropString); boolean introduceVirtualExternalChange = false; List<String> newExternals = new ArrayList<String>(); SVNURL ownerURL = getWcContext().getNodeUrl(externalHolder); if (ownerURL == null) { continue; } long ownerRev = getWcContext().getNodeBaseRev(externalHolder); File ownerReposRelPath = getWcContext().getNodeReposRelPath(externalHolder); File sourceReposRelPath = getWcContext().getNodeReposRelPath(svnCopyPair.source); String relativePath = SVNWCUtils.getPathAsChild(sourceReposRelPath, ownerReposRelPath); SVNURL targetURL = svnCopyPair.dst.appendPath(relativePath, false); for (int k = 0; k < externals.length; k++) { File externalWC = new File(externalHolder, externals[k].getPath()); SVNRevision externalsWCRevision = SVNRevision.UNDEFINED; try { long rev = getWcContext().getNodeBaseRev(externalWC); if (rev >= 0) { externalsWCRevision = SVNRevision.create(rev); } } catch (SVNException e) { // smthing went wrong. } 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 = getOperation().getExternalsHandler().handleExternal( externalWC, resolvedURL, externals[k].getRevision(), externals[k].getPegRevision(), externals[k].getRawValue(), externalsWCRevision); if (revs != null && revs.length == 2 && !revs[0].equals(externals[k].getRevision())) { 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; } } else if (revs != null) { newExternals.add(externals[k].getRawValue()); } } if (introduceVirtualExternalChange) { String newExternalsProp = ""; for (String external : newExternals) { newExternalsProp += external + '\n'; } SvnCommitItem itemWithExternalsChanges = packet.getItem(externalHolder); if (itemWithExternalsChanges == null) { itemWithExternalsChanges = packet.addItem(externalHolder, repos.getRepositoryRoot(true), SVNNodeKind.DIR, targetURL, -1, ownerURL, ownerRev, SvnCommitItem.PROPS_MODIFIED); } itemWithExternalsChanges.addOutgoingProperty(SVNProperty.EXTERNALS, SVNPropertyValue.create(newExternalsProp)); } } } private Collection<SVNURL> findMissingParents(SVNURL targetURL, SVNRepository repository) throws SVNException { SVNNodeKind kind = repository.checkPath("", -1); Collection<SVNURL> parents = new ArrayList<SVNURL>(); while (kind == SVNNodeKind.NONE) { parents.add(targetURL); targetURL = targetURL.removePathTail(); repository.setLocation(targetURL, false); kind = repository.checkPath("", -1); } if (kind != SVNNodeKind.DIR) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_ALREADY_EXISTS, "Path ''{0}'' already exists, but it is not a directory", targetURL); SVNErrorManager.error(err, SVNLogType.WC); } return parents; } private File getCommonCopyAncestor(Collection<SvnCopyPair> copyPairs) { File ancestor = null; for (SvnCopyPair svnCopyPair : copyPairs) { if (ancestor == null) { ancestor = svnCopyPair.source; continue; } String ancestorPath = ancestor.getAbsolutePath().replace(File.separatorChar, '/'); String sourcePath = svnCopyPair.source.getAbsolutePath().replace(File.separatorChar, '/'); ancestorPath = SVNPathUtil.getCommonPathAncestor(ancestorPath, sourcePath); ancestor = new File(ancestorPath); } return ancestor; } private Map<String, SVNMergeRangeList> calculateTargetMergeInfo(File srcFile, long srcRevision, SVNRepository repository) throws SVNException { SVNURL url = null; SVNURL oldLocation = null; Structure<NodeOriginInfo> nodeOrigin = getWcContext().getNodeOrigin(srcFile, false, NodeOriginInfo.revision, NodeOriginInfo.reposRelpath, NodeOriginInfo.reposRootUrl); if (nodeOrigin != null && nodeOrigin.get(NodeOriginInfo.reposRelpath) != null) { url = nodeOrigin.get(NodeOriginInfo.reposRootUrl); url = SVNWCUtils.join(url, nodeOrigin.<File>get(NodeOriginInfo.reposRelpath)); srcRevision = nodeOrigin.lng(NodeOriginInfo.revision); } if (url != null) { Map<String, SVNMergeRangeList> targetMergeInfo = null; String mergeInfoPath; SVNRepository repos = repository; try { mergeInfoPath = getRepositoryAccess().getPathRelativeToSession(url, null, repos); if (mergeInfoPath == null) { oldLocation = repos.getLocation(); repos.setLocation(url, false); mergeInfoPath = ""; } targetMergeInfo = getRepositoryAccess().getReposMergeInfo(repos, mergeInfoPath, srcRevision, SVNMergeInfoInheritance.INHERITED, true); } finally { if (repository == null) { repos.closeSession(); } else if (oldLocation != null) { repos.setLocation(oldLocation, false); } } return targetMergeInfo; } return null; } private static class SvnCopyPair { File source; SVNURL dst; } public SVNNodeKind getUrlKind(SVNURL url, long revision) throws SVNException { return getRepositoryAccess().createRepository(url, null).checkPath("", revision); } }