/* * ==================================================================== * 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.io.fs; import java.io.OutputStream; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Stack; 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.SVNNodeKind; import org.tmatesoft.svn.core.SVNProperties; import org.tmatesoft.svn.core.SVNProperty; import org.tmatesoft.svn.core.SVNPropertyValue; import org.tmatesoft.svn.core.SVNRevisionProperty; import org.tmatesoft.svn.core.internal.util.SVNDate; import org.tmatesoft.svn.core.internal.util.SVNPathUtil; import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; import org.tmatesoft.svn.core.io.ISVNDeltaConsumer; import org.tmatesoft.svn.core.io.ISVNEditor; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.diff.SVNDiffWindow; import org.tmatesoft.svn.util.SVNLogType; import org.tmatesoft.svn.util.Version; /** * @version 1.3 * @author TMate Software Ltd. */ public class FSCommitEditor implements ISVNEditor { private Map<String, String> myPathsToLockTokens; private Collection<String> myLockTokens; private String myBasePath; private FSTransactionInfo myTxn; private FSTransactionRoot myTxnRoot; private boolean isTxnOwner; private FSFS myFSFS; private FSRepository myRepository; private Stack<DirBaton> myDirsStack; private FSDeltaConsumer myDeltaConsumer; private SVNProperties myCurrentFileProps; private String myCurrentFilePath; private FSCommitter myCommitter; private SVNProperties myRevProps; private String myAuthor; public FSCommitEditor(String path, String logMessage, String userName, Map<String, String> lockTokens, boolean keepLocks, FSTransactionInfo txn, FSFS owner, FSRepository repository) { this(path, lockTokens, keepLocks, txn, owner, repository, null); myRevProps = new SVNProperties(); if (userName != null) { myAuthor = userName; myRevProps.put(SVNRevisionProperty.AUTHOR, userName); } if (logMessage != null) { myRevProps.put(SVNRevisionProperty.LOG, logMessage); } myRevProps.put(SVNRevisionProperty.SVN_TXN_CLIENT_COMPAT_VERSION, Version.getSVNVersion()); } public FSCommitEditor(String path, Map<String, String> lockTokens, boolean keepLocks, FSTransactionInfo txn, FSFS owner, FSRepository repository, SVNProperties revProps) { myPathsToLockTokens = !keepLocks ? lockTokens : null; myLockTokens = lockTokens != null ? lockTokens.values() : new HashSet<String>(); myBasePath = path; myTxn = txn; isTxnOwner = txn == null; myRepository = repository; myFSFS = owner; myDirsStack = new Stack<DirBaton>(); myRevProps = revProps != null ? revProps : new SVNProperties(); if (!myRevProps.containsName(SVNRevisionProperty.SVN_TXN_CLIENT_COMPAT_VERSION)) { myRevProps.put(SVNRevisionProperty.SVN_TXN_CLIENT_COMPAT_VERSION, Version.getSVNVersion()); } } public void targetRevision(long revision) throws SVNException { // does nothing } public void openRoot(long revision) throws SVNException { long youngestRev = myFSFS.getYoungestRevision(); if (isTxnOwner) { myTxn = FSTransactionRoot.beginTransactionForCommit(youngestRev, myRevProps, myFSFS); } else { myFSFS.changeTransactionProperties(myTxn.getTxnId(), myRevProps); } myTxnRoot = myFSFS.createTransactionRoot(myTxn); myCommitter = new FSCommitter(myFSFS, myTxnRoot, myTxn, myLockTokens, getAuthor()); DirBaton dirBaton = new DirBaton(revision, myBasePath, false); myDirsStack.push(dirBaton); } private String getAuthor() { if (myAuthor == null) { myAuthor = myRevProps.getStringValue(SVNRevisionProperty.AUTHOR); } return myAuthor; } public void openDir(String path, long revision) throws SVNException { DirBaton parentBaton = (DirBaton) myDirsStack.peek(); String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)); SVNNodeKind kind = myTxnRoot.checkNodeKind(fullPath); if (kind == SVNNodeKind.NONE) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_DIRECTORY, "Path ''{0}'' not present", path); SVNErrorManager.error(err, SVNLogType.FSFS); } DirBaton dirBaton = new DirBaton(revision, fullPath, parentBaton.isCopied()); myDirsStack.push(dirBaton); } public void deleteEntry(String path, long revision) throws SVNException { String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)); SVNNodeKind kind = myTxnRoot.checkNodeKind(fullPath); if (kind == SVNNodeKind.NONE) { SVNErrorManager.error(FSErrors.errorOutOfDate(fullPath, kind), SVNLogType.FSFS); } FSRevisionNode existingNode = myTxnRoot.getRevisionNode(fullPath); long createdRev = existingNode.getCreatedRevision(); if (FSRepository.isValidRevision(revision) && revision < createdRev) { SVNErrorManager.error(FSErrors.errorOutOfDate(fullPath, kind), SVNLogType.FSFS); } myCommitter.deleteNode(fullPath); } public void absentDir(String path) throws SVNException { // does nothing } public void absentFile(String path) throws SVNException { // does nothing } public void addDir(String path, String copyFromPath, long copyFromRevision) throws SVNException { DirBaton parentBaton = (DirBaton) myDirsStack.peek(); String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)); boolean isCopied = false; if (copyFromPath != null && FSRepository.isInvalidRevision(copyFromRevision)) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_GENERAL, "Got source path but no source revision for ''{0}''", fullPath); SVNErrorManager.error(err, SVNLogType.FSFS); } else if (copyFromPath != null) { SVNNodeKind kind = myTxnRoot.checkNodeKind(fullPath); if (kind != SVNNodeKind.NONE && !parentBaton.isCopied()) { SVNErrorManager.error(FSErrors.errorOutOfDate(fullPath, kind), SVNLogType.FSFS); } copyFromPath = myRepository.getRepositoryPath(copyFromPath); FSRevisionRoot copyRoot = myFSFS.createRevisionRoot(copyFromRevision); myCommitter.makeCopy(copyRoot, copyFromPath, fullPath, true); isCopied = true; } else { myCommitter.makeDir(fullPath); } DirBaton dirBaton = new DirBaton(SVNRepository.INVALID_REVISION, fullPath, isCopied); myDirsStack.push(dirBaton); } public void changeDirProperty(String name, SVNPropertyValue value) throws SVNException { DirBaton dirBaton = (DirBaton) myDirsStack.peek(); if (FSRepository.isValidRevision(dirBaton.getBaseRevision())) { FSRevisionNode existingNode = myTxnRoot.getRevisionNode(dirBaton.getPath()); long createdRev = existingNode.getCreatedRevision(); if (dirBaton.getBaseRevision() < createdRev) { SVNErrorManager.error(FSErrors.errorOutOfDate(dirBaton.getPath(), SVNNodeKind.DIR), SVNLogType.FSFS); } } myCommitter.changeNodeProperty(dirBaton.getPath(), name, value); } private void changeNodeProperties(String path, SVNProperties propNamesToValues) throws SVNException { FSParentPath parentPath = null; SVNNodeKind kind = null; SVNProperties properties = null; boolean done = false; boolean haveRealChanges = false; for (Iterator<String> propNames = propNamesToValues.nameSet().iterator(); propNames.hasNext();) { String propName = (String)propNames.next(); SVNPropertyValue propValue = propNamesToValues.getSVNPropertyValue(propName); FSRepositoryUtil.validateProperty(propName, propValue); if (!done) { parentPath = myTxnRoot.openPath(path, true, true); kind = parentPath.getRevNode().getType(); if ((myTxnRoot.getTxnFlags() & FSTransactionRoot.SVN_FS_TXN_CHECK_LOCKS) != 0) { myCommitter.allowLockedOperation(myFSFS, path, getAuthor(), myLockTokens, false, false); } myCommitter.makePathMutable(parentPath, path); properties = parentPath.getRevNode().getProperties(myFSFS); done = true; } if (properties.isEmpty() && propValue == null) { continue; } if (myFSFS.supportsMergeInfo() && propName.equals(SVNProperty.MERGE_INFO)) { long increment = 0; boolean hadMergeInfo = parentPath.getRevNode().hasMergeInfo(); if (propValue != null && !hadMergeInfo) { increment = 1; } else if (propValue == null && hadMergeInfo) { increment = -1; } if (increment != 0) { parentPath.getRevNode().setHasMergeInfo(propValue != null); myCommitter.incrementMergeInfoUpTree(parentPath, increment); } } if (propValue == null) { properties.remove(propName); } else { properties.put(propName, propValue); } if (!haveRealChanges) { haveRealChanges = true; } } if (haveRealChanges) { myTxnRoot.setProplist(parentPath.getRevNode(), properties); myCommitter.addChange(path, parentPath.getRevNode().getId(), FSPathChangeKind.FS_PATH_CHANGE_MODIFY, false, true, SVNRepository.INVALID_REVISION, null, kind); } } public void closeDir() throws SVNException { flushPendingProperties(); myDirsStack.pop(); } public void addFile(String path, String copyFromPath, long copyFromRevision) throws SVNException { DirBaton parentBaton = (DirBaton) myDirsStack.peek(); String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)); if (copyFromPath != null && FSRepository.isInvalidRevision(copyFromRevision)) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_GENERAL, "Got source path but no source revision for ''{0}''", fullPath); SVNErrorManager.error(err, SVNLogType.FSFS); } else if (copyFromPath != null) { SVNNodeKind kind = myTxnRoot.checkNodeKind(fullPath); if (kind != SVNNodeKind.NONE && !parentBaton.isCopied()) { SVNErrorManager.error(FSErrors.errorOutOfDate(fullPath, kind), SVNLogType.FSFS); } copyFromPath = myRepository.getRepositoryPath(copyFromPath); FSRevisionRoot copyRoot = myFSFS.createRevisionRoot(copyFromRevision); myCommitter.makeCopy(copyRoot, copyFromPath, fullPath, true); } else { myCommitter.makeFile(fullPath); } } public void openFile(String path, long revision) throws SVNException { String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)); FSRevisionNode revNode = myTxnRoot.getRevisionNode(fullPath); if (FSRepository.isValidRevision(revision) && revision < revNode.getCreatedRevision()) { SVNErrorManager.error(FSErrors.errorOutOfDate(fullPath, SVNNodeKind.FILE), SVNLogType.FSFS); } } public void applyTextDelta(String path, String baseChecksum) throws SVNException { flushPendingProperties(); ISVNDeltaConsumer fsfsConsumer = getDeltaConsumer(); fsfsConsumer.applyTextDelta(path, baseChecksum); } public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException { ISVNDeltaConsumer fsfsConsumer = getDeltaConsumer(); return fsfsConsumer.textDeltaChunk(path, diffWindow); } public void textDeltaEnd(String path) throws SVNException { ISVNDeltaConsumer fsfsConsumer = getDeltaConsumer(); fsfsConsumer.textDeltaEnd(path); } private FSDeltaConsumer getDeltaConsumer() { if (myDeltaConsumer == null) { myDeltaConsumer = new FSDeltaConsumer(myBasePath, myTxnRoot, myFSFS, myCommitter, getAuthor(), myLockTokens); } return myDeltaConsumer; } public void changeFileProperty(String path, String name, SVNPropertyValue value) throws SVNException { String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)); SVNProperties props = getFilePropertiesStorage(); if (!fullPath.equals(myCurrentFilePath)) { if (myCurrentFilePath != null) { changeNodeProperties(myCurrentFilePath, props); props.clear(); } myCurrentFilePath = fullPath; } props.put(name, value); } private SVNProperties getFilePropertiesStorage() { if (myCurrentFileProps == null) { myCurrentFileProps = new SVNProperties(); } return myCurrentFileProps; } private void flushPendingProperties() throws SVNException { if (myCurrentFilePath != null) { SVNProperties props = getFilePropertiesStorage(); changeNodeProperties(myCurrentFilePath, props); props.clear(); myCurrentFilePath = null; } } public void closeFile(String path, String textChecksum) throws SVNException { flushPendingProperties(); if (textChecksum != null) { String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)); FSRevisionNode revNode = myTxnRoot.getRevisionNode(fullPath); FSRepresentation txtRep = revNode.getTextRepresentation(); if (txtRep != null && !textChecksum.equals(txtRep.getMD5HexDigest())) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH, "Checksum mismatch for resulting fulltext\n({0}):\n expected checksum: {1}\n actual checksum: {2}\n", new Object[] { fullPath, textChecksum, txtRep.getMD5HexDigest() }); SVNErrorManager.error(err, SVNLogType.FSFS); } } } public SVNCommitInfo closeEdit() throws SVNException { try { if (myTxn == null) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS, "No valid transaction supplied to closeEdit()"); SVNErrorManager.error(err, SVNLogType.FSFS); } long committedRev = -1; if (myDeltaConsumer != null) { myDeltaConsumer.close(); } SVNErrorMessage[] errorMessage = new SVNErrorMessage[1]; committedRev = myCommitter.commitTxn(true, true, errorMessage, null); SVNProperties revProps = myFSFS.getRevisionProperties(committedRev); String dateProp = revProps.getStringValue(SVNRevisionProperty.DATE); Date datestamp = dateProp != null ? SVNDate.parseDateString(dateProp) : null; SVNErrorMessage err = errorMessage[0]; if (err != null && err.getErrorCode() == SVNErrorCode.REPOS_POST_COMMIT_HOOK_FAILED) { String message = err.getChildErrorMessage() != null ? err.getChildErrorMessage().getFullMessage() : err.getFullMessage(); err = SVNErrorMessage.create(SVNErrorCode.REPOS_POST_COMMIT_HOOK_FAILED, message, SVNErrorMessage.TYPE_WARNING); } SVNCommitInfo info = new SVNCommitInfo(committedRev, getAuthor(), datestamp, err); releaseLocks(); return info; } finally { myRepository.closeRepository(); } } private void releaseLocks() { releaseLocks(myPathsToLockTokens, false, true); final Map<String, String> autoUnlockPaths = myCommitter.getAutoUnlockPaths(); releaseLocks(autoUnlockPaths, true, false); } private void releaseLocks(Map<String, String> pathsToLockTokens, boolean breakLocks, boolean runHooks) { if (pathsToLockTokens == null) { return; } for (Iterator<String> paths = pathsToLockTokens.keySet().iterator(); paths.hasNext();) { String path = (String) paths.next(); String token = (String) pathsToLockTokens.get(path); String absPath = !path.startsWith("/") ? SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myBasePath, path)) : path; try { myFSFS.unlockPath(absPath, token, getAuthor(), breakLocks, runHooks); } catch (SVNException svne) { // ignore exceptions } } } public void abortEdit() throws SVNException { if (myDeltaConsumer != null) { myDeltaConsumer.abort(); } try { if (myTxn == null || !isTxnOwner) { return; } FSCommitter.abortTransaction(myFSFS, myTxn.getTxnId()); } finally { myRepository.closeRepository(); myTxn = null; myTxnRoot = null; } } private static class DirBaton { private long myBaseRevision; private String myPath; private boolean isCopied; public DirBaton(long revision, String path, boolean copied) { myBaseRevision = revision; myPath = path; isCopied = copied; } public boolean isCopied() { return isCopied; } public long getBaseRevision() { return myBaseRevision; } public String getPath() { return myPath; } } }