/*
* ====================================================================
* Copyright (c) 2004-2011 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.wc17;
import java.io.File;
import java.io.FilterInputStream;
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 java.util.TreeSet;
import java.util.logging.Level;
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.wc.ISVNCommitPathHandler;
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.admin.SVNChecksumInputStream;
import org.tmatesoft.svn.core.internal.wc.admin.SVNChecksumOutputStream;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext.PristineContentsInfo;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext.WritableBaseInfo;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbInfo.InfoField;
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.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.core.wc2.SvnCommitItem;
import org.tmatesoft.svn.util.SVNLogType;
/**
* @version 1.4
* @author TMate Software Ltd.
*/
public class SVNCommitter17 implements ISVNCommitPathHandler {
private SVNWCContext myContext;
private Map<String, SvnCommitItem> myCommittables;
private SVNURL myRepositoryRoot;
private Map<File, SvnChecksum> myMd5Checksums;
private Map<File, SvnChecksum> mySha1Checksums;
private Map<String, SvnCommitItem> myModifiedFiles;
private SVNDeltaGenerator myDeltaGenerator;
private Collection<File> deletedPaths;
public SVNCommitter17(SVNWCContext context, Map<String, SvnCommitItem> committables, SVNURL repositoryRoot, Collection<File> tmpFiles, Map<File, SvnChecksum> md5Checksums,
Map<File, SvnChecksum> sha1Checksums) {
myContext = context;
myCommittables = committables;
myRepositoryRoot = repositoryRoot;
myMd5Checksums = md5Checksums;
mySha1Checksums = sha1Checksums;
myModifiedFiles = new TreeMap<String, SvnCommitItem>();
deletedPaths = new TreeSet<File>();
}
public static SVNCommitInfo commit(SVNWCContext context, Collection<File> tmpFiles, Map<String, SvnCommitItem> committables, SVNURL repositoryRoot, ISVNEditor commitEditor,
Map<File, SvnChecksum> md5Checksums, Map<File, SvnChecksum> sha1Checksums) throws SVNException {
SVNCommitter17 committer = new SVNCommitter17(context, committables, repositoryRoot, tmpFiles, md5Checksums, sha1Checksums);
SVNCommitUtil.driveCommitEditor(committer, committables.keySet(), commitEditor, -1);
committer.sendTextDeltas(commitEditor);
return commitEditor.closeEdit();
}
public Collection<File> getDeletedPaths() {
return deletedPaths;
}
public boolean handleCommitPath(String commitPath, ISVNEditor commitEditor) throws SVNException {
SvnCommitItem item = myCommittables.get(commitPath);
myContext.checkCancelled();
if (item.hasFlag(SvnCommitItem.COPY)) {
if (item.getCopyFromUrl() == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Commit item ''{0}'' has copy flag but no copyfrom URL", item.getPath());
SVNErrorManager.error(err, SVNLogType.WC);
} else if (item.getCopyFromRevision() < 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Commit item ''{0}'' has copy flag but an invalid revision", item.getPath());
SVNErrorManager.error(err, SVNLogType.WC);
}
}
boolean closeDir = false;
File localAbspath = null;
if (item.getKind() != SVNNodeKind.NONE && item.getPath() != null) {
localAbspath = item.getPath();
}
long rev = item.getRevision();
SVNEvent event = null;
if (item.hasFlag(SvnCommitItem.ADD) && item.hasFlag(SvnCommitItem.DELETE)) {
event = SVNEventFactory.createSVNEvent(localAbspath, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_REPLACED, null, null, null);
event.setPreviousRevision(rev);
} else if (item.hasFlag(SvnCommitItem.DELETE)) {
event = SVNEventFactory.createSVNEvent(localAbspath, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_DELETED, null, null, null);
event.setPreviousRevision(rev);
} else if (item.hasFlag(SvnCommitItem.ADD)) {
String mimeType = null;
if (item.getKind() == SVNNodeKind.FILE && localAbspath != null) {
mimeType = myContext.getProperty(localAbspath, SVNProperty.MIME_TYPE);
}
event = SVNEventFactory.createSVNEvent(localAbspath, item.getKind(), mimeType, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_ADDED, null, null, null);
event.setPreviousRevision(item.getCopyFromRevision() >= 0 ? item.getCopyFromRevision() : -1);
event.setPreviousURL(item.getCopyFromUrl());
} else if (item.hasFlag(SvnCommitItem.TEXT_MODIFIED) || item.hasFlag(SvnCommitItem.PROPS_MODIFIED)) {
SVNStatusType contentState = SVNStatusType.UNCHANGED;
if (item.hasFlag(SvnCommitItem.TEXT_MODIFIED)) {
contentState = SVNStatusType.CHANGED;
}
SVNStatusType propState = SVNStatusType.UNCHANGED;
if (item.hasFlag(SvnCommitItem.PROPS_MODIFIED) ) {
propState = SVNStatusType.CHANGED;
}
event = SVNEventFactory.createSVNEvent(localAbspath, item.getKind(), null, SVNRepository.INVALID_REVISION, contentState, propState, null, SVNEventAction.COMMIT_MODIFIED, null, null, null);
event.setPreviousRevision(rev);
}
if (event != null) {
event.setURL(item.getUrl());
if (myContext.getEventHandler() != null) {
myContext.getEventHandler().handleEvent(event, ISVNEventHandler.UNKNOWN);
}
}
if (item.hasFlag(SvnCommitItem.DELETE)) {
try {
commitEditor.deleteEntry(commitPath, rev);
} catch (SVNException e) {
fixError(localAbspath, commitPath, e, item.getKind());
}
if (!item.hasFlag(SvnCommitItem.ADD)) {
deletedPaths.add(localAbspath);
}
}
long cfRev = item.getCopyFromRevision();
Map<String, SVNPropertyValue> outgoingProperties = item.getOutgoingProperties();
boolean fileOpen = false;
if (item.hasFlag(SvnCommitItem.ADD)) {
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<String> propsIter = outgoingProperties.keySet().iterator(); propsIter.hasNext();) {
String propName = propsIter.next();
SVNPropertyValue propValue = outgoingProperties.get(propName);
if (item.getKind() == SVNNodeKind.FILE) {
commitEditor.changeFileProperty(commitPath, propName, propValue);
} else {
commitEditor.changeDirProperty(propName, propValue);
}
}
outgoingProperties = null;
}
}
if (item.hasFlag(SvnCommitItem.PROPS_MODIFIED)) { // || (outgoingProperties != null && !outgoingProperties.isEmpty())) {
if (item.getKind() == SVNNodeKind.FILE) {
if (!fileOpen) {
try {
commitEditor.openFile(commitPath, rev);
} catch (SVNException e) {
fixError(localAbspath, commitPath, e, SVNNodeKind.FILE);
}
}
fileOpen = true;
} else if (!item.hasFlag(SvnCommitItem.ADD)) {
// do not open dir twice.
try {
if ("".equals(commitPath)) {
commitEditor.openRoot(rev);
} else {
commitEditor.openDir(commitPath, rev);
}
} catch (SVNException svne) {
fixError(localAbspath, commitPath, svne, SVNNodeKind.DIR);
}
closeDir = true;
}
if (item.hasFlag(SvnCommitItem.PROPS_MODIFIED)) {
try {
sendPropertiesDelta(localAbspath, commitPath, item, commitEditor);
} catch (SVNException e) {
fixError(localAbspath, commitPath, e, item.getKind());
}
}
if (outgoingProperties != null) {
for (Iterator<String> propsIter = outgoingProperties.keySet().iterator(); propsIter.hasNext();) {
String propName = propsIter.next();
SVNPropertyValue propValue = outgoingProperties.get(propName);
if (item.getKind() == SVNNodeKind.FILE) {
commitEditor.changeFileProperty(commitPath, propName, propValue);
} else {
commitEditor.changeDirProperty(propName, propValue);
}
}
}
}
if (item.hasFlag(SvnCommitItem.TEXT_MODIFIED) && item.getKind() == SVNNodeKind.FILE) {
if (!fileOpen) {
try {
commitEditor.openFile(commitPath, rev);
} catch (SVNException e) {
fixError(localAbspath, commitPath, e, SVNNodeKind.FILE);
}
}
myModifiedFiles.put(commitPath, item);
} else if (fileOpen) {
try {
commitEditor.closeFile(commitPath, null);
} catch (SVNException e) {
fixError(localAbspath, commitPath, e, SVNNodeKind.FILE);
}
}
return closeDir;
}
private void fixError(File localAbspath, String path, SVNException e, SVNNodeKind kind) throws SVNException {
SVNErrorMessage err = e.getErrorMessage();
if (err.getErrorCode() == SVNErrorCode.FS_NOT_FOUND ||
err.getErrorCode() == SVNErrorCode.FS_ALREADY_EXISTS ||
err.getErrorCode() == SVNErrorCode.FS_TXN_OUT_OF_DATE ||
err.getErrorCode() == SVNErrorCode.RA_DAV_PATH_NOT_FOUND ||
err.getErrorCode() == SVNErrorCode.RA_DAV_ALREADY_EXISTS ||
err.hasChildWithErrorCode(SVNErrorCode.RA_OUT_OF_DATE)) {
if (myContext.getEventHandler() != null) {
SVNEvent event;
if (localAbspath != null) {
event = SVNEventFactory.createSVNEvent(localAbspath, kind, null, -1, SVNEventAction.FAILED_OUT_OF_DATE, null, err, null);
} else {
//TODO: add baseUrl parameter
//TODO: add url-based events
// event = SVNEventFactory.createSVNEvent(new File(path).getAbsoluteFile(), kind, null, -1, SVNEventAction.FAILED_OUT_OF_DATE, null, err, null);
event = null;
}
if (event != null) {
myContext.getEventHandler().handleEvent(event, ISVNEventHandler.UNKNOWN);
}
}
err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_UP_TO_DATE,
kind == SVNNodeKind.DIR ?
"Directory ''{0}'' is out of date" :
"File ''{0}'' is out of date",
localAbspath);
throw new SVNException(err);
} else if (err.hasChildWithErrorCode(SVNErrorCode.FS_NO_LOCK_TOKEN) ||
err.getErrorCode() == SVNErrorCode.FS_LOCK_OWNER_MISMATCH ||
err.getErrorCode() == SVNErrorCode.RA_NOT_LOCKED) {
if (myContext.getEventHandler() != null) {
SVNEvent event;
if (localAbspath != null) {
event = SVNEventFactory.createSVNEvent(localAbspath, kind, null, -1, SVNEventAction.FAILED_LOCKED, null, err, null);
} else {
//TODO
event = null;
}
if (event != null) {
myContext.getEventHandler().handleEvent(event, ISVNEventHandler.UNKNOWN);
}
}
err = SVNErrorMessage.create(SVNErrorCode.CLIENT_NO_LOCK_TOKEN,
kind == SVNNodeKind.DIR ?
"Directory ''{0}'' is locked in another working copy" :
"File ''{0}'' is locked in another working copy",
localAbspath);
throw new SVNException(err);
} else if (err.hasChildWithErrorCode(SVNErrorCode.RA_DAV_FORBIDDEN) ||
err.getErrorCode() == SVNErrorCode.AUTHZ_UNWRITABLE) {
if (myContext.getEventHandler() != null) {
SVNEvent event;
if (localAbspath != null) {
event = SVNEventFactory.createSVNEvent(localAbspath, kind, null, -1, SVNEventAction.FAILED_FORBIDDEN_BY_SERVER, null, err, null);
} else {
//TODO
event = null;
}
if (event != null) {
myContext.getEventHandler().handleEvent(event, ISVNEventHandler.UNKNOWN);
}
err = SVNErrorMessage.create(SVNErrorCode.CLIENT_FORBIDDEN_BY_SERVER,
kind == SVNNodeKind.DIR ?
"Changing directory ''{0}'' is forbidden by the server" :
"Changing file ''{0}'' is forbidden by the server",
localAbspath);
throw new SVNException(err);
}
}
throw e;
}
private String getCopyFromPath(SVNURL url) {
if (url == null) {
return null;
}
String path = url.getPath();
if (myRepositoryRoot.getPath().equals(path)) {
return "/";
}
return path.substring(myRepositoryRoot.getPath().length());
}
private void sendPropertiesDelta(File localAbspath, String commitPath, SvnCommitItem item, ISVNEditor commitEditor) throws SVNException {
SVNNodeKind kind = myContext.readKind(localAbspath, false);
SVNProperties propMods = myContext.getPropDiffs(localAbspath).propChanges;
for (Object i : propMods.nameSet()) {
String propName = (String) i;
SVNPropertyValue propValue = propMods.getSVNPropertyValue(propName);
if (kind == SVNNodeKind.FILE) {
commitEditor.changeFileProperty(commitPath, propName, propValue);
} else {
commitEditor.changeDirProperty(propName, propValue);
}
}
}
public void sendTextDeltas(ISVNEditor editor) throws SVNException {
for (String path : myModifiedFiles.keySet()) {
SvnCommitItem item = myModifiedFiles.get(path);
myContext.checkCancelled();
File itemAbspath = item.getPath();
if (myContext.getEventHandler() != null) {
SVNEvent event = SVNEventFactory.createSVNEvent(itemAbspath, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_DELTA_SENT, null, null, null);
myContext.getEventHandler().handleEvent(event, ISVNEventHandler.UNKNOWN);
}
boolean fulltext = item.hasFlag(SvnCommitItem.ADD);
TransmittedChecksums transmitTextDeltas = transmitTextDeltas(path, itemAbspath, fulltext, editor);
SvnChecksum newTextBaseMd5Checksum = transmitTextDeltas.md5Checksum;
SvnChecksum newTextBaseSha1Checksum = transmitTextDeltas.sha1Checksum;
if (myMd5Checksums != null) {
myMd5Checksums.put(itemAbspath, newTextBaseMd5Checksum);
}
if (mySha1Checksums != null) {
mySha1Checksums.put(itemAbspath, newTextBaseSha1Checksum);
}
}
}
private static class TransmittedChecksums {
public SvnChecksum md5Checksum;
public SvnChecksum sha1Checksum;
}
private TransmittedChecksums transmitTextDeltas(String path, File localAbspath, boolean fulltext, ISVNEditor editor) throws SVNException {
InputStream localStream = SVNFileUtil.DUMMY_IN;
InputStream baseStream = SVNFileUtil.DUMMY_IN;
SvnChecksum expectedMd5Checksum = null;
SvnChecksum localMd5Checksum = null;
SvnChecksum verifyChecksum = null;
SVNChecksumOutputStream localSha1ChecksumStream = null;
SVNChecksumInputStream verifyChecksumStream = null;
SVNErrorMessage error = null;
File newPristineTmpAbspath = null;
try {
localStream = myContext.getTranslatedStream(localAbspath, localAbspath, true, false);
WritableBaseInfo openWritableBase = myContext.openWritableBase(localAbspath, false, true);
OutputStream newPristineStream = openWritableBase.stream;
newPristineTmpAbspath = openWritableBase.tempBaseAbspath;
localSha1ChecksumStream = openWritableBase.sha1ChecksumStream;
localStream = new CopyingStream(newPristineStream, localStream);
File baseFile = null;
if (!fulltext) {
PristineContentsInfo pristineContents = myContext.getPristineContents(localAbspath, true, true);
baseFile = pristineContents.path;
baseStream = pristineContents.stream;
if (baseStream == null) {
baseStream = SVNFileUtil.DUMMY_IN;
}
expectedMd5Checksum = myContext.getDb().readInfo(localAbspath, InfoField.checksum).checksum;
if (expectedMd5Checksum != null && expectedMd5Checksum.getKind() != SvnChecksum.Kind.md5) {
expectedMd5Checksum = myContext.getDb().getPristineMD5(localAbspath, expectedMd5Checksum);
}
if (expectedMd5Checksum != null) {
verifyChecksumStream = new SVNChecksumInputStream(baseStream, SVNChecksumInputStream.MD5_ALGORITHM);
baseStream = verifyChecksumStream;
} else {
expectedMd5Checksum = new SvnChecksum(SvnChecksum.Kind.md5, SVNFileUtil.computeChecksum(baseFile));
}
}
editor.applyTextDelta(path, expectedMd5Checksum!=null ? expectedMd5Checksum.getDigest() : null);
if (myDeltaGenerator == null) {
myDeltaGenerator = new SVNDeltaGenerator();
}
localMd5Checksum = new SvnChecksum(SvnChecksum.Kind.md5, myDeltaGenerator.sendDelta(path, baseStream, 0, localStream, editor, true));
if (verifyChecksumStream != null) {
//SVNDeltaGenerator#sendDelta doesn't guarantee to read the whole stream (e.g. if baseStream has no data, it is not touched at all)
//so we read verifyChecksumStream to force MD5 calculation
readRemainingStream(verifyChecksumStream, baseFile);
verifyChecksum = new SvnChecksum(SvnChecksum.Kind.md5, verifyChecksumStream.getDigest());
}
} catch (SVNException svne) {
error = svne.getErrorMessage().wrap("While preparing ''{0}'' for commit", localAbspath);
} finally {
SVNFileUtil.closeFile(localStream);
SVNFileUtil.closeFile(baseStream);
}
if (expectedMd5Checksum != null && verifyChecksum != null && !expectedMd5Checksum.equals(verifyChecksum)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH, "Checksum mismatch for ''{0}''; expected: ''{1}'', actual: ''{2}''", new Object[] {
localAbspath, expectedMd5Checksum.getDigest(), verifyChecksum.getDigest()
});
SVNErrorMessage corruptedBaseErr = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT_TEXT_BASE);
SVNErrorManager.error(corruptedBaseErr, err, SVNLogType.WC);
}
if (error != null) {
SVNErrorManager.error(error, SVNLogType.WC);
}
try {
editor.closeFile(path, localMd5Checksum!=null ? localMd5Checksum.getDigest() : null);
} catch (SVNException e) {
fixError(localAbspath, path, e, SVNNodeKind.FILE);
}
SvnChecksum localSha1Checksum = new SvnChecksum(SvnChecksum.Kind.sha1, localSha1ChecksumStream.getDigest());
myContext.getDb().installPristine(newPristineTmpAbspath, localSha1Checksum, localMd5Checksum);
TransmittedChecksums result = new TransmittedChecksums();
result.md5Checksum = localMd5Checksum;
result.sha1Checksum = localSha1Checksum;
return result;
}
private void readRemainingStream(SVNChecksumInputStream verifyChecksumStream, File sourceFile) throws SVNException {
final byte[] buffer = new byte[1024];
int bytesRead;
do {
try {
bytesRead = verifyChecksumStream.read(buffer);
} catch (IOException e) {
SVNErrorMessage err;
if (sourceFile != null) {
err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot read from file ''{0}'': {1}", new Object[]{
sourceFile, e.getMessage()
});
} else {
err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot read from stream: {0}", new Object[]{
sourceFile, e.getMessage()
});
}
SVNErrorManager.error(err, Level.FINE, SVNLogType.WC);
return;
}
} while (bytesRead >= 0);
}
private class CopyingStream extends FilterInputStream {
private OutputStream myOutput;
public CopyingStream(OutputStream out, InputStream in) {
super(in);
myOutput = out;
}
public int read() throws IOException {
int r = super.read();
if (r != -1) {
myOutput.write(r);
}
return r;
}
public int read(byte[] b) throws IOException {
int r = super.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 = super.read(b, off, len);
if (r != -1) {
myOutput.write(b, off, r);
}
return r;
}
public void close() throws IOException {
try{
myOutput.close();
} finally {
super.close();
}
}
}
}