package org.tmatesoft.svn.core.internal.wc2.ng; import java.io.File; import java.util.*; import java.util.logging.Level; import org.tmatesoft.sqljet.core.SqlJetTransactionMode; import org.tmatesoft.svn.core.*; import org.tmatesoft.svn.core.internal.db.SVNSqlJetDb; import org.tmatesoft.svn.core.internal.util.SVNDate; import org.tmatesoft.svn.core.internal.util.SVNPathUtil; import org.tmatesoft.svn.core.internal.util.SVNSkel; import org.tmatesoft.svn.core.internal.wc.SVNCommitUtil; import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; import org.tmatesoft.svn.core.internal.wc.SVNEventFactory; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.core.internal.wc.SVNPropertiesManager; import org.tmatesoft.svn.core.internal.wc17.SVNCommitMediator17; import org.tmatesoft.svn.core.internal.wc17.SVNCommitter17; import org.tmatesoft.svn.core.internal.wc17.SVNWCContext; import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbKind; import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbStatus; import org.tmatesoft.svn.core.internal.wc17.db.Structure; import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.NodeInfo; import org.tmatesoft.svn.core.internal.wc2.ISvnCommitRunner; 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.ISVNEventHandler; import org.tmatesoft.svn.core.wc.SVNEventAction; import org.tmatesoft.svn.core.wc2.SvnChecksum; import org.tmatesoft.svn.core.wc2.SvnCommit; import org.tmatesoft.svn.core.wc2.SvnCommitItem; import org.tmatesoft.svn.core.wc2.SvnCommitPacket; import org.tmatesoft.svn.core.wc2.SvnTarget; import org.tmatesoft.svn.util.SVNDebugLog; import org.tmatesoft.svn.util.SVNLogType; public class SvnNgCommit extends SvnNgOperationRunner<SVNCommitInfo, SvnCommit> implements ISvnCommitRunner, ISvnUrlKindCallback { public SvnCommitPacket collectCommitItems(SvnCommit operation) throws SVNException { int depthEmptyAfter = -1; setOperation(operation); SvnCommitPacket packet = new SvnCommitPacket(); Collection<String> targets = new ArrayList<String>(); String[] validatedPaths = new String[getOperation().getTargets().size()]; int i = 0; for(SvnTarget target : getOperation().getTargets()) { validatedPaths[i] = target.getFile().getAbsolutePath(); validatedPaths[i] = validatedPaths[i].replace(File.separatorChar, '/'); i++; } String rootPath = SVNPathUtil.condencePaths(validatedPaths, targets, false); if (rootPath == null) { return packet; } File baseDir = new File(rootPath).getAbsoluteFile(); if (targets.isEmpty()) { targets.add(""); } if (getOperation().isIncludeFileExternals() || getOperation().isIncludeDirectoryExternals()) { if (getOperation().getDepth() != SVNDepth.UNKNOWN && getOperation().getDepth() != SVNDepth.INFINITY) { depthEmptyAfter = targets.size(); } appendExternalsAsExplicitTargets(targets, baseDir, getOperation().isIncludeFileExternals(), getOperation().isIncludeDirectoryExternals(), getOperation().getDepth(), getWcContext()); } Collection<File> lockTargets = determineLockTargets(baseDir, targets); Collection<File> lockedRoots = new HashSet<File>(); try { for (File lockTarget : lockTargets) { File lockRoot = getWcContext().acquireWriteLock(lockTarget, false, true); lockedRoots.add(lockRoot); } packet.setLockingContext(this, lockedRoots); Map<SVNURL, String> lockTokens = new HashMap<SVNURL, String>(); SvnNgCommitUtil.harvestCommittables(getWcContext(), packet, lockTokens, baseDir, targets, depthEmptyAfter, getOperation().getDepth(), !getOperation().isKeepLocks(), getOperation().getApplicableChangelists(), this, getOperation().getCommitParameters(), null); packet.setLockTokens(lockTokens); if (getOperation().isFailOnMultipleRepositories() && packet.getRepositoryRoots().size() > 1) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Commit can only commit to a single repository at a time.\n" + "Are all targets part of the same working copy?"); SVNErrorManager.error(err, SVNLogType.WC); } if (!packet.isEmpty()) { return packet; } else { packet.dispose(); return new SvnCommitPacket(); } } catch (SVNException e) { packet.dispose(); SVNErrorMessage err = e.getErrorMessage().wrap("Commit failed (details follow):"); SVNErrorManager.error(err, SVNLogType.WC); } return null; } private void appendExternalsAsExplicitTargets(Collection<String> targets, File baseAbsPath, boolean includeFileExternals, boolean includeDirectoryExternals, SVNDepth depth, SVNWCContext context) throws SVNException { if (!(includeFileExternals||includeDirectoryExternals)) { return; } if (depth == SVNDepth.EMPTY) { return; } List<String> newTargets = new ArrayList<String>(); for (String target : targets) { File targetAbsPath = SVNFileUtil.createFilePath(baseAbsPath, target); List<SVNWCContext.CommittableExternalInfo> externals = context.committableExternalsBelow(null, targetAbsPath, depth); if (externals != null) { for (SVNWCContext.CommittableExternalInfo xInfo : externals) { if ((xInfo.kind == SVNNodeKind.FILE && !includeFileExternals)|| (xInfo.kind == SVNNodeKind.DIR && !includeDirectoryExternals)) { continue; } File targetRelPath = SVNFileUtil.skipAncestor(baseAbsPath, xInfo.localAbsPath); assert targetRelPath != null && SVNFileUtil.getFilePath(targetRelPath).length() != 0; newTargets.add(SVNFileUtil.getFilePath(targetRelPath)); } } } targets.addAll(newTargets); } @Override protected SVNCommitInfo run(SVNWCContext context) throws SVNException { final SvnCommitPacket[] packets = getOperation().splitCommitPackets(getOperation().isCombinePackets()); SVNCommitInfo result = SVNCommitInfo.NULL; for(int i = 0; i < packets.length; i++) { if (packets[i] == null || packets[i].isEmpty()) { continue; } packets[i] = packets[i].removeSkippedItems(); final SVNURL repositoryRoot = packets[i].getRepositoryRoots().iterator().next(); result = doRun(context, packets[i]); if (result != null) { getOperation().receive(SvnTarget.fromURL(repositoryRoot), result); } } return result; } protected SVNCommitInfo doRun(SVNWCContext context, SvnCommitPacket packet) throws SVNException { SVNProperties revisionProperties = getOperation().getRevisionProperties(); SVNPropertiesManager.validateRevisionProperties(revisionProperties); SVNException bumpError = null; SVNCommitInfo info = null; try { for (SVNURL repositoryRoot : packet.getRepositoryRoots()) { Collection<SvnCommitItem> items = packet.getItems(repositoryRoot); for (SvnCommitItem item : items) { if (item.hasFlag(SvnCommitItem.MOVED_HERE)) { SVNWCContext.NodeMovedHere nodeMovedHere = context.nodeWasMovedHere(item.getPath()); File movedFromAbsPath = nodeMovedHere.movedFromAbsPath; File deleteOpRootAbsPath = nodeMovedHere.deleteOpRootAbsPath; if (movedFromAbsPath != null && deleteOpRootAbsPath != null && movedFromAbsPath.equals(deleteOpRootAbsPath)) { boolean foundDeleteHalf = packet.getItem(deleteOpRootAbsPath) != null; if (!foundDeleteHalf) { File deleteHalfParentAbsPath = SVNFileUtil.getFileDir(deleteOpRootAbsPath); if (!deleteOpRootAbsPath.equals(deleteHalfParentAbsPath)) { File parentDeleteOpRootAbsPath = context.getNodeDeletedAncestor(deleteHalfParentAbsPath); if (parentDeleteOpRootAbsPath != null) { foundDeleteHalf = packet.getItem(parentDeleteOpRootAbsPath) != null; } } } if (!foundDeleteHalf) { SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, "Cannot commit ''{0}'' because it was moved from " + "''{1}'' which is not part of the commit; both " + "sides of the move must be committed together", item.getPath(), deleteOpRootAbsPath); SVNErrorManager.error(errorMessage, SVNLogType.WC); } } } if (item.hasFlag(SvnCommitItem.DELETE)) { SVNWCContext.NodeMovedAway nodeMovedAway = context.nodeWasMovedAway(item.getPath()); File movedToAbsPath = nodeMovedAway.movedToAbsPath; File copyOpRootAbsPath = nodeMovedAway.opRootAbsPath; if (movedToAbsPath != null && copyOpRootAbsPath != null && movedToAbsPath.equals(copyOpRootAbsPath) && packet.getItem(copyOpRootAbsPath) == null) { SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, "Cannot commit ''{0}'' because it was moved to ''{1}'' " + "which is not part of the commit; both sides of the move must be committed together", item.getPath(), copyOpRootAbsPath); SVNErrorManager.error(errorMessage, SVNLogType.WC); } } } } String commitMessage = getOperation().getCommitMessage(); if (getOperation().getCommitHandler() != null) { Collection<SvnCommitItem> items = new ArrayList<SvnCommitItem>(); for (SVNURL rootUrl : packet.getRepositoryRoots()) { items.addAll(packet.getItems(rootUrl)); } SvnCommitItem[] itemsArray = items.toArray(new SvnCommitItem[items.size()]); try { commitMessage = getOperation().getCommitHandler().getCommitMessage(commitMessage, itemsArray); if (commitMessage == null) { return SVNCommitInfo.NULL; } revisionProperties = getOperation().getCommitHandler().getRevisionProperties(commitMessage, itemsArray, revisionProperties); } catch (SVNException e) { SVNErrorMessage err = e.getErrorMessage().wrap("Commit failed (details follow):"); SVNErrorManager.error(err, SVNLogType.WC); } } commitMessage = commitMessage == null ? "" : SVNCommitUtil.validateCommitMessage(commitMessage); boolean keepLocks = getOperation().isKeepLocks(); SVNURL repositoryRootUrl = packet.getRepositoryRoots().iterator().next(); if (packet.isEmpty(repositoryRootUrl)) { return SVNCommitInfo.NULL; } Map<String, SvnCommitItem> committables = new TreeMap<String, SvnCommitItem>(); Map<File, SvnChecksum> md5Checksums = new HashMap<File, SvnChecksum>(); Map<File, SvnChecksum> sha1Checksums = new HashMap<File, SvnChecksum>(); SVNURL baseURL = SvnNgCommitUtil.translateCommitables(packet.getItems(repositoryRootUrl), committables); Map<String, String> lockTokens = SvnNgCommitUtil.translateLockTokens(packet.getLockTokens(), baseURL); SvnCommitItem firstItem = packet.getItems(repositoryRootUrl).iterator().next(); SVNRepository repository = getRepositoryAccess().createRepository(baseURL, firstItem.getPath()); SVNCommitMediator17 mediator = new SVNCommitMediator17(context, committables); ISVNEditor commitEditor = null; try { commitEditor = repository.getCommitEditor(commitMessage, lockTokens, keepLocks, revisionProperties, mediator); SVNCommitter17 committer = new SVNCommitter17(context, committables, repositoryRootUrl, mediator.getTmpFiles(), md5Checksums, sha1Checksums); SVNCommitUtil.driveCommitEditor(committer, committables.keySet(), commitEditor, -1); committer.sendTextDeltas(commitEditor); info = commitEditor.closeEdit(); commitEditor = null; if (info.getErrorMessage() == null || info.getErrorMessage().getErrorCode() == SVNErrorCode.REPOS_POST_COMMIT_HOOK_FAILED) { // do some post processing, make sure not to unlock wc (to dipose packet) in case there // is an error on post processing. SvnCommittedQueue queue = new SvnCommittedQueue(); try { for (SvnCommitItem item : packet.getItems(repositoryRootUrl)) { postProcessCommitItem(queue, item, getOperation().isKeepChangelists(), getOperation().isKeepLocks(), sha1Checksums.get(item.getPath())); } processCommittedQueue(queue, info.getNewRevision(), info.getDate(), info.getAuthor()); deleteDeleteFiles(committer, getOperation().getCommitParameters()); } catch (SVNException e) { // this is bump error. bumpError = e; throw e; } finally { // only for the last packet in chain. if (packet.isLastPacket()) { sleepForTimestamp(); } } } handleEvent(SVNEventFactory.createSVNEvent(null, SVNNodeKind.NONE, null, info.getNewRevision(), SVNEventAction.COMMIT_COMPLETED, SVNEventAction.COMMIT_COMPLETED, null, null, -1, -1)); } catch (SVNException e) { if (e instanceof SVNCancelException) { throw e; } SVNErrorMessage err = e.getErrorMessage().wrap("Commit failed (details follow):"); info = new SVNCommitInfo(-1, null, null, err); handleEvent(SVNEventFactory.createErrorEvent(err, SVNEventAction.COMMIT_COMPLETED), ISVNEventHandler.UNKNOWN); if (packet.getRepositoryRoots().size() == 1) { SVNErrorManager.error(err, SVNLogType.WC); } } finally { if (commitEditor != null) { try { commitEditor.abortEdit(); } catch (SVNException e) { SVNDebugLog.getDefaultLog().log(SVNLogType.CLIENT, e, Level.WARNING); //the exception should not mask original exception } } for (File tmpFile : mediator.getTmpFiles()) { SVNFileUtil.deleteFile(tmpFile); } } } finally { if (bumpError == null) { packet.dispose(); } } return info; } private void postProcessCommitItem(SvnCommittedQueue queue, SvnCommitItem item, boolean keepChangelists, boolean keepLocks, SvnChecksum sha1Checksum) throws SVNException { boolean removeLock = !keepLocks && item.hasFlag(SvnCommitItem.LOCK); Map<String, SVNPropertyValue> wcPropChanges = item.getIncomingProperties(); SVNProperties wcProps = null; if (wcPropChanges != null) { try { wcProps = getWcContext().getDb().getBaseDavCache(item.getPath()); } catch (SVNException e) { // missing properties. } if (wcProps == null) { wcProps = new SVNProperties(); } for (String name : wcPropChanges.keySet()) { SVNPropertyValue pv = wcPropChanges.get(name); if (pv == null) { wcProps.remove(name); } else { wcProps.put(name, pv); } } } final boolean unlockOnly = item.getFlags() == SvnCommitItem.LOCK && wcPropChanges == null; queueCommitted(queue, item.getPath(), false, wcProps, unlockOnly, removeLock, !keepChangelists, sha1Checksum); } public SVNNodeKind getUrlKind(SVNURL url, long revision) throws SVNException { return getRepositoryAccess().createRepository(url, null).checkPath("", revision); } private Collection<File> determineLockTargets(File baseDirectory, Collection<String> targets) throws SVNException { Map<File, Collection<File>> wcItems = new HashMap<File, Collection<File>>(); for (String t: targets) { File target = SVNFileUtil.createFilePath(baseDirectory, t); File wcRoot = null; try { wcRoot = getWcContext().getDb().getWCRoot(target); } catch (SVNException e) { if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND) { continue; } throw e; } Collection<File> wcTargets = wcItems.get(wcRoot); if (wcTargets == null) { wcTargets = new HashSet<File>(); wcItems.put(wcRoot, wcTargets); } wcTargets.add(target); } Collection<File> lockTargets = new HashSet<File>(); for (File wcRoot : wcItems.keySet()) { Collection<File> wcTargets = wcItems.get(wcRoot); if (wcTargets.size() == 1) { if (wcRoot.equals(wcTargets.iterator().next())) { lockTargets.add(wcRoot); } else { lockTargets.add(SVNFileUtil.getParentFile(wcTargets.iterator().next())); } } else if (wcTargets.size() > 1) { lockTargets.add(wcRoot); } } return lockTargets; } public Object splitLockingContext(Object lockingContext, SvnCommitPacket newPacket) { if (!(lockingContext instanceof Collection)) { return lockingContext; } @SuppressWarnings("unchecked") final Collection<File> lockedPaths = (Collection<File>) lockingContext; final Collection<File> newLockedPaths = new ArrayList<File>(); for (SVNURL root : newPacket.getRepositoryRoots()) { for(SvnCommitItem item : newPacket.getItems(root)) { final File path = item.getPath(); for (File lockedPath : lockedPaths) { if (path.equals(lockedPath) || (path.isFile() && path.getParentFile().equals(lockedPath))) { newLockedPaths.add(lockedPath); break; } } } } return newLockedPaths; }; public void disposeCommitPacket(Object lockingContext, boolean disposeParentContext) throws SVNException { if (!(lockingContext instanceof Collection)) { if (disposeParentContext) { getWcContext().close(); } return; } if (disposeParentContext) { @SuppressWarnings("unchecked") Collection<File> lockedPaths = (Collection<File>) lockingContext; for (File lockedPath : lockedPaths) { try { getWcContext().releaseWriteLock(lockedPath); } catch (SVNException e) { if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_LOCKED) { throw e; } } } getWcContext().close(); } } private void queueCommitted(SvnCommittedQueue queue, File localAbsPath, boolean recurse, SVNProperties wcPropChanges, boolean unlockOnly, boolean removeLock, boolean removeChangelist, SvnChecksum sha1Checksum) { SvnCommittedQueueItem cqi = new SvnCommittedQueueItem(); cqi.localAbspath = localAbsPath; cqi.recurse = recurse; cqi.noUnlock = !removeLock; cqi.keepChangelist = !removeChangelist; cqi.sha1Checksum = sha1Checksum; cqi.newDavCache = wcPropChanges; cqi.unlockOnly = unlockOnly; queue.queue.put(localAbsPath, cqi); } private void processCommittedQueue(SvnCommittedQueue queue, long newRevision, Date revDate, String revAuthor) throws SVNException { final Map<File, Collection<SvnCommittedQueueItem>> itemsMap = new HashMap<File, Collection<SvnCommittedQueueItem>>(); for (final SvnCommittedQueueItem cqi : queue.queue.values()) { final File root = getWcContext().getDb().getWCRoot(cqi.localAbspath); if (!itemsMap.containsKey(root)) { itemsMap.put(root, new ArrayList<SvnNgCommit.SvnCommittedQueueItem>()); } itemsMap.get(root).add(cqi); } queue.queue.clear(); for (final File root : itemsMap.keySet()) { final SVNSqlJetDb db = getWcContext().getDb().getSDb(root); db.beginTransaction(SqlJetTransactionMode.WRITE); try { for (SvnCommittedQueueItem cqi : itemsMap.get(root)) { processCommittedInternal(cqi.localAbspath, cqi.recurse, true, newRevision, new SVNDate(revDate.getTime(), 0), revAuthor, cqi.newDavCache, cqi.unlockOnly, cqi.noUnlock, cqi.keepChangelist, cqi.sha1Checksum, queue); } getWcContext().wqRun(root); } catch (SVNException th) { db.rollback(); throw th; } finally { db.commit(); } } } private void processCommittedInternal(File localAbspath, boolean recurse, boolean topOfRecurse, long newRevision, SVNDate revDate, String revAuthor, SVNProperties newDavCache, boolean unlockOnly, boolean noUnlock, boolean keepChangelist, SvnChecksum sha1Checksum, SvnCommittedQueue queue) throws SVNException { processCommittedLeaf(localAbspath, !topOfRecurse, newRevision, revDate, revAuthor, newDavCache, unlockOnly, noUnlock, keepChangelist, sha1Checksum); } private void processCommittedLeaf(File localAbspath, boolean viaRecurse, long newRevnum, SVNDate newChangedDate, String newChangedAuthor, SVNProperties newDavCache, boolean unlockOnly, boolean noUnlock, boolean keepChangelist, SvnChecksum checksum) throws SVNException { long newChangedRev = newRevnum; assert (SVNFileUtil.isAbsolute(localAbspath)); Structure<NodeInfo> nodeInfo = getWcContext().getDb().readInfo(localAbspath, NodeInfo.status, NodeInfo.kind, NodeInfo.checksum, NodeInfo.hadProps, NodeInfo.propsMod, NodeInfo.haveBase, NodeInfo.haveWork); File admAbspath; if (nodeInfo.get(NodeInfo.kind) == SVNWCDbKind.Dir) { admAbspath = localAbspath; } else { admAbspath = SVNFileUtil.getFileDir(localAbspath); } getWcContext().writeCheck(admAbspath); if (nodeInfo.get(NodeInfo.status) == SVNWCDbStatus.Deleted) { getWcContext().getDb().removeBase(localAbspath, false, false, true, !viaRecurse ? newRevnum : SVNRepository.INVALID_REVISION, null, null); nodeInfo.release(); return; } else if (nodeInfo.get(NodeInfo.status) == SVNWCDbStatus.NotPresent) { nodeInfo.release(); return; } if (unlockOnly) { if (!noUnlock) { getWcContext().getDb().removeLock(localAbspath); } } else { SVNSkel workItem = null; SVNWCDbKind kind = nodeInfo.get(NodeInfo.kind); if (kind != SVNWCDbKind.Dir) { if (checksum == null) { checksum = nodeInfo.get(NodeInfo.checksum); if (viaRecurse && !nodeInfo.is(NodeInfo.propsMod)) { Structure<NodeInfo> moreInfo = getWcContext().getDb(). readInfo(localAbspath, NodeInfo.changedRev, NodeInfo.changedDate, NodeInfo.changedAuthor); newChangedRev = moreInfo.lng(NodeInfo.changedRev); newChangedDate = moreInfo.get(NodeInfo.changedDate); newChangedAuthor = moreInfo.get(NodeInfo.changedAuthor); moreInfo.release(); } } workItem = getWcContext().wqBuildFileCommit(localAbspath, nodeInfo.is(NodeInfo.propsMod)); } getWcContext().getDb().globalCommit(localAbspath, newRevnum, newChangedRev, newChangedDate, newChangedAuthor, checksum, null, newDavCache, keepChangelist, noUnlock, workItem); } } private static class SvnCommittedQueue { @SuppressWarnings("unchecked") public Map<File, SvnCommittedQueueItem> queue = new TreeMap<File, SvnCommittedQueueItem>(SVNCommitUtil.FILE_COMPARATOR); }; private static class SvnCommittedQueueItem { public File localAbspath; public boolean recurse; public boolean noUnlock; public boolean keepChangelist; public SvnChecksum sha1Checksum; public SVNProperties newDavCache; public boolean unlockOnly; } }