/*
* ====================================================================
* Copyright (c) 2004-2009 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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Iterator;
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.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.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNChecksumInputStream;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.internal.wc.admin.SVNVersionedProperties;
import org.tmatesoft.svn.core.internal.wc.admin.SVNWCAccess;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNCommitItem;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.util.SVNLogType;
/**
* @version 1.3
* @author TMate Software Ltd.
*/
public class SVNCommitter implements ISVNCommitPathHandler {
private Map myCommitItems;
private Map myModifiedFiles;
private Collection myTmpFiles;
private String myRepositoryRoot;
private SVNDeltaGenerator myDeltaGenerator;
public SVNCommitter(Map commitItems, String reposRoot, Collection tmpFiles) {
myCommitItems = commitItems;
myModifiedFiles = new TreeMap();
myTmpFiles = tmpFiles;
myRepositoryRoot = reposRoot;
}
public boolean handleCommitPath(String commitPath, ISVNEditor commitEditor) throws SVNException {
SVNCommitItem item = (SVNCommitItem) myCommitItems.get(commitPath);
SVNWCAccess wcAccess = item.getWCAccess();
wcAccess.checkCancelled();
if (item.isCopied()) {
if (item.getCopyFromURL() == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Commit item ''{0}'' has copy flag but no copyfrom URL", item.getFile());
SVNErrorManager.error(err, SVNLogType.WC);
} else if (item.getRevision().getNumber() < 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Commit item ''{0}'' has copy flag but an invalid revision", item.getFile());
SVNErrorManager.error(err, SVNLogType.WC);
}
}
SVNEvent event = null;
boolean closeDir = false;
File file = null;
if (item.getFile() != null) {
file = item.getFile();
} else if (item.getPath() != null) {
file = new File(wcAccess.getAnchor(), item.getPath());
}
long rev = item.getRevision().getNumber();
if (item.isAdded() && item.isDeleted()) {
event = SVNEventFactory.createSVNEvent(file, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_REPLACED, null, null, null);
event.setPreviousRevision(rev);
} else if (item.isAdded()) {
String mimeType = null;
if (item.getKind() == SVNNodeKind.FILE && file != null) {
SVNAdminArea dir = item.getWCAccess().retrieve(file.getParentFile());
mimeType = dir.getProperties(file.getName()).getStringPropertyValue(SVNProperty.MIME_TYPE);
}
event = SVNEventFactory.createSVNEvent(file, item.getKind(), mimeType, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_ADDED, null, null, null);
event.setPreviousRevision(item.getCopyFromRevision() != null ? item.getCopyFromRevision().getNumber() : -1);
event.setPreviousURL(item.getCopyFromURL());
} else if (item.isDeleted()) {
event = SVNEventFactory.createSVNEvent(file, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_DELETED, null, null, null);
event.setPreviousRevision(rev);
} else if (item.isContentsModified() || item.isPropertiesModified()) {
event = SVNEventFactory.createSVNEvent(file, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_MODIFIED, null, null, null);
event.setPreviousRevision(rev);
}
if (event != null) {
event.setURL(item.getURL());
wcAccess.handleEvent(event, ISVNEventHandler.UNKNOWN);
}
long cfRev = item.getCopyFromRevision().getNumber();//item.getCopyFromURL() != null ? rev : -1;
if (item.isDeleted()) {
try {
commitEditor.deleteEntry(commitPath, rev);
} catch (SVNException e) {
fixError(commitPath, e, SVNNodeKind.FILE);
}
}
boolean fileOpen = false;
Map outgoingProperties = item.getOutgoingProperties();
if (item.isAdded()) {
String copyFromPath = getCopyFromPath(item.getCopyFromURL());
if (item.getKind() == SVNNodeKind.FILE) {
commitEditor.addFile(commitPath, copyFromPath, cfRev);
fileOpen = true;
} else {
commitEditor.addDir(commitPath, copyFromPath, cfRev);
closeDir = true;
}
if (outgoingProperties != null) {
for (Iterator propsIter = outgoingProperties.keySet().iterator(); propsIter.hasNext();) {
String propName = (String) propsIter.next();
SVNPropertyValue propValue = (SVNPropertyValue) outgoingProperties.get(propName);
if (item.getKind() == SVNNodeKind.FILE) {
commitEditor.changeFileProperty(commitPath, propName, propValue);
} else {
commitEditor.changeDirProperty(propName, propValue);
}
}
outgoingProperties = null;
}
}
if (item.isPropertiesModified()) {
if (item.getKind() == SVNNodeKind.FILE) {
if (!fileOpen) {
try {
commitEditor.openFile(commitPath, rev);
} catch (SVNException e) {
fixError(commitPath, e, SVNNodeKind.FILE);
}
}
fileOpen = true;
} else if (!item.isAdded()) {
// do not open dir twice.
try {
if ("".equals(commitPath)) {
commitEditor.openRoot(rev);
} else {
commitEditor.openDir(commitPath, rev);
}
} catch (SVNException svne) {
fixError(commitPath, svne, SVNNodeKind.DIR);
}
closeDir = true;
}
try {
sendPropertiesDelta(commitPath, item, commitEditor);
} catch (SVNException e) {
fixError(commitPath, e, item.getKind());
}
if (outgoingProperties != null) {
for (Iterator propsIter = outgoingProperties.keySet().iterator(); propsIter.hasNext();) {
String propName = (String) propsIter.next();
SVNPropertyValue propValue = (SVNPropertyValue) outgoingProperties.get(propName);
if (item.getKind() == SVNNodeKind.FILE) {
commitEditor.changeFileProperty(commitPath, propName, propValue);
} else {
commitEditor.changeDirProperty(propName, propValue);
}
}
}
}
if (item.isContentsModified() && item.getKind() == SVNNodeKind.FILE) {
if (!fileOpen) {
try {
commitEditor.openFile(commitPath, rev);
} catch (SVNException e) {
fixError(commitPath, e, SVNNodeKind.FILE);
}
}
myModifiedFiles.put(commitPath, item);
} else if (fileOpen) {
commitEditor.closeFile(commitPath, null);
}
return closeDir;
}
public void sendTextDeltas(ISVNEditor editor) throws SVNException {
for (Iterator paths = myModifiedFiles.keySet().iterator(); paths.hasNext();) {
String path = (String) paths.next();
SVNCommitItem item = (SVNCommitItem) myModifiedFiles.get(path);
SVNWCAccess wcAccess = item.getWCAccess();
wcAccess.checkCancelled();
SVNEvent event = SVNEventFactory.createSVNEvent(new File(wcAccess.getAnchor(), item.getPath()),SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_DELTA_SENT, null, null, null);
wcAccess.handleEvent(event, ISVNEventHandler.UNKNOWN);
SVNAdminArea dir = wcAccess.retrieve(item.getFile().getParentFile());
String name = SVNPathUtil.tail(item.getPath());
SVNEntry entry = dir.getEntry(name, false);
File tmpFile = dir.getBaseFile(name, true);
myTmpFiles.add(tmpFile);
String expectedChecksum = null;
String checksum = null;
String newChecksum = null;
SVNChecksumInputStream baseChecksummedIS = null;
InputStream sourceIS = null;
InputStream targetIS = null;
OutputStream tmpBaseStream = null;
File baseFile = dir.getBaseFile(name, false);
SVNErrorMessage error = null;
boolean useChecksummedStream = false;
boolean openSrcStream = false;
if (!item.isAdded()) {
openSrcStream = true;
expectedChecksum = entry.getChecksum();
if (expectedChecksum != null) {
useChecksummedStream = true;
} else {
expectedChecksum = SVNFileUtil.computeChecksum(baseFile);
}
} else {
sourceIS = SVNFileUtil.DUMMY_IN;
}
editor.applyTextDelta(path, expectedChecksum);
if (myDeltaGenerator == null) {
myDeltaGenerator = new SVNDeltaGenerator();
}
try {
sourceIS = openSrcStream ? SVNFileUtil.openFileForReading(baseFile, SVNLogType.WC) : sourceIS;
if (useChecksummedStream) {
baseChecksummedIS = new SVNChecksumInputStream(sourceIS, SVNChecksumInputStream.MD5_ALGORITHM, true, true);
sourceIS = baseChecksummedIS;
}
targetIS = SVNTranslator.getTranslatedStream(dir, name, true, false);
tmpBaseStream = SVNFileUtil.openFileForWriting(tmpFile);
CopyingStream localStream = new CopyingStream(tmpBaseStream, targetIS);
newChecksum = myDeltaGenerator.sendDelta(path, sourceIS, 0, localStream, editor, true);
} catch (SVNException svne) {
error = svne.getErrorMessage().wrap("While preparing ''{0}'' for commit", dir.getFile(name));
} finally {
SVNFileUtil.closeFile(sourceIS);
SVNFileUtil.closeFile(targetIS);
SVNFileUtil.closeFile(tmpBaseStream);
}
if (baseChecksummedIS != null) {
checksum = baseChecksummedIS.getDigest();
}
if (expectedChecksum != null && checksum != null && !expectedChecksum.equals(checksum)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT_TEXT_BASE,
"Checksum mismatch for ''{0}''; expected: ''{1}'', actual: ''{2}''",
new Object[] { dir.getFile(name), expectedChecksum, checksum });
SVNErrorManager.error(err, SVNLogType.WC);
}
if (error != null) {
SVNErrorManager.error(error, SVNLogType.WC);
}
editor.closeFile(path, newChecksum);
}
}
private void sendPropertiesDelta(String commitPath, SVNCommitItem item, ISVNEditor editor) throws SVNException {
SVNAdminArea dir;
String name;
SVNWCAccess wcAccess = item.getWCAccess();
if (item.getKind() == SVNNodeKind.DIR) {
dir = wcAccess.retrieve(item.getFile());
name = "";
} else {
dir = wcAccess.retrieve(item.getFile().getParentFile());
name = SVNPathUtil.tail(item.getPath());
}
if (!dir.hasPropModifications(name)) {
return;
}
SVNEntry entry = dir.getEntry(name, false);
boolean replaced = false;
if (entry != null) {
replaced = entry.isScheduledForReplacement();
}
SVNVersionedProperties props = dir.getProperties(name);
SVNVersionedProperties baseProps = replaced ? null : dir.getBaseProperties(name);
SVNProperties diff = replaced ? props.asMap() : baseProps.compareTo(props).asMap();
if (diff != null && !diff.isEmpty()) {
File tmpPropsFile = dir.getPropertiesFile(name, true);
SVNWCProperties tmpProps = new SVNWCProperties(tmpPropsFile, null);
for(Iterator propNames = props.getPropertyNames(null).iterator(); propNames.hasNext();) {
String propName = (String) propNames.next();
SVNPropertyValue propValue = props.getPropertyValue(propName);
tmpProps.setPropertyValue(propName, propValue);
}
if (!tmpPropsFile.exists()) {
// create empty tmp (!) file just to make sure it will be used on post-commit.
SVNFileUtil.createEmptyFile(tmpPropsFile);
}
myTmpFiles.add(tmpPropsFile);
for (Iterator names = diff.nameSet().iterator(); names.hasNext();) {
String propName = (String) names.next();
SVNPropertyValue value = diff.getSVNPropertyValue(propName);
if (item.getKind() == SVNNodeKind.FILE) {
editor.changeFileProperty(commitPath, propName, value);
} else {
editor.changeDirProperty(propName, value);
}
}
}
}
private String getCopyFromPath(SVNURL url) {
if (url == null) {
return null;
}
String path = url.getPath();
if (myRepositoryRoot.equals(path)) {
return "/";
}
return path.substring(myRepositoryRoot.length());
}
private void fixError(String path, SVNException e, SVNNodeKind kind) throws SVNException {
SVNErrorMessage err = e.getErrorMessage();
if (err.getErrorCode() == SVNErrorCode.FS_NOT_FOUND || err.getErrorCode() == SVNErrorCode.RA_DAV_PATH_NOT_FOUND) {
err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_UP_TO_DATE, kind == SVNNodeKind.DIR ?
"Directory ''{0}'' is out of date" : "File ''{0}'' is out of date", path);
throw new SVNException(err);
}
throw e;
}
public static SVNCommitInfo commit(Collection tmpFiles, Map commitItems, String repositoryRoot, ISVNEditor commitEditor) throws SVNException {
SVNCommitter committer = new SVNCommitter(commitItems, repositoryRoot, tmpFiles);
SVNCommitUtil.driveCommitEditor(committer, commitItems.keySet(), commitEditor, -1);
committer.sendTextDeltas(commitEditor);
return commitEditor.closeEdit();
}
private class CopyingStream extends InputStream {
private InputStream myInput;
private OutputStream myOutput;
public CopyingStream(OutputStream os, InputStream is) {
myInput = is;
myOutput = os;
}
public int read() throws IOException {
int r = myInput.read();
if (r != -1) {
myOutput.write(r);
}
return r;
}
public int read(byte[] b) throws IOException {
int r = myInput.read(b);
if (r != -1) {
myOutput.write(b, 0, r);
}
return r;
}
public int read(byte[] b, int off, int len) throws IOException {
int r = myInput.read(b, off, len);
if (r != -1) {
myOutput.write(b, off, r);
}
return r;
}
}
}