package org.tmatesoft.svn.core.internal.wc2.ng; import java.io.File; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.tmatesoft.svn.core.ISVNDirEntryHandler; import org.tmatesoft.svn.core.SVNCommitInfo; import org.tmatesoft.svn.core.SVNDepth; import org.tmatesoft.svn.core.SVNDirEntry; 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.internal.util.SVNPathUtil; 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.SVNChecksumOutputStream; import org.tmatesoft.svn.core.internal.wc17.SVNWCContext; import org.tmatesoft.svn.core.io.ISVNEditor; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.diff.SVNDeltaProcessor; import org.tmatesoft.svn.core.io.diff.SVNDiffWindow; 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.util.SVNLogType; public class SvnNgRemoteDiffEditor implements ISVNEditor { private SVNWCContext context; private File target; private SVNRepository repository; private long revision; private boolean walkDeletedDirs; private boolean pureRemoteDiff; private ISvnDiffCallback diffCallback; private long targetRevision; private File emptyFile; private Map<File, DeletedPath> deletedPaths; private DirBaton currentDir; private SvnDiffCallbackResult currentResult; private FileBaton currentFile; private ISVNEventHandler eventHandler; private File globalTmpDir; private Collection<File> tmpFiles; public static SvnNgRemoteDiffEditor createEditor(SVNWCContext context, File target, SVNDepth depth, SVNRepository repository, long revision, boolean walkDeletedDirs, boolean dryRun, boolean pureRemoteDiff, ISvnDiffCallback diffCallback, ISVNEventHandler handler) { SvnNgRemoteDiffEditor editor = new SvnNgRemoteDiffEditor(); editor.context = context; editor.target = target; editor.repository = repository; editor.revision = revision; editor.walkDeletedDirs = walkDeletedDirs; editor.diffCallback = diffCallback; editor.deletedPaths = new HashMap<File, DeletedPath>(); editor.pureRemoteDiff = pureRemoteDiff; editor.tmpFiles = new ArrayList<File>(); editor.currentResult = new SvnDiffCallbackResult(); editor.eventHandler = handler; return editor; } public File getGlobalTmpDir() throws SVNException { if (globalTmpDir == null) { globalTmpDir = SVNFileUtil.createTempDirectory("svndiff"); } return globalTmpDir; } private static class DeletedPath { SVNNodeKind kind; SVNEventAction action; SVNStatusType state; } private static class DirBaton { boolean added; boolean treeConflicted; boolean skip; boolean skipChildren; File wcPath; DirBaton parent; SVNProperties propChanges; SVNProperties pristineProperties; public void loadProperties(SVNRepository repos, String path, long revision) throws SVNException { pristineProperties = new SVNProperties(); repos.getDir(path, revision, pristineProperties, 0, (ISVNDirEntryHandler) null); } } private class FileBaton { boolean added; boolean treeConflicted; boolean skip; String repoPath; File wcPath; File startRevisionFile; File endRevisionFile; SVNProperties pristineProps; long baseRevision; // SvnChecksum startMd5Checksum; SvnChecksum resultMd5Checksum; SVNProperties propChanges; public SVNDeltaProcessor deltaProcessor; public void loadFile(SVNWCContext context, SVNRepository repos, boolean propsOnly, Collection<File> tmpFiles) throws SVNException { if (!propsOnly) { File tmpDir = pureRemoteDiff ? getGlobalTmpDir() : context.getDb().getWCRootTempDir(wcPath); startRevisionFile = SVNFileUtil.createUniqueFile(tmpDir, "diff", ".tmp", false); tmpFiles.add(startRevisionFile); OutputStream os = null; try { os = SVNFileUtil.openFileForWriting(startRevisionFile); os = new SVNChecksumOutputStream(os, "MD5", true); this.pristineProps = new SVNProperties(); repos.getFile(this.repoPath, this.baseRevision, this.pristineProps, os); } finally { SVNFileUtil.closeFile(os); } } else { this.pristineProps = new SVNProperties(); repos.getFile(this.repoPath, this.baseRevision, this.pristineProps, null); } } public String[] getMimeTypes() { String[] r = new String[2]; if (pristineProps != null) { r[0] = pristineProps.getStringValue(SVNProperty.MIME_TYPE); r[1] = r[0]; } if (propChanges != null) { r[1] = propChanges.getStringValue(SVNProperty.MIME_TYPE); } return r; } } private DirBaton makeDirBaton(String path, DirBaton parent, boolean added) { final DirBaton baton = new DirBaton(); baton.parent = parent; baton.added = added; baton.wcPath = SVNFileUtil.createFilePath(target, path); baton.propChanges = new SVNProperties(); return baton; } private FileBaton makeFileBaton(String path, boolean added) { final FileBaton baton = new FileBaton(); baton.added = added; baton.repoPath = path; baton.wcPath = SVNFileUtil.createFilePath(target, path); baton.propChanges = new SVNProperties(); baton.baseRevision = this.revision; return baton; } private void handleEvent(SVNEvent event) throws SVNException { if (event != null && eventHandler != null) { eventHandler.handleEvent(event, -1); } } public void applyTextDelta(String path, String baseChecksum) throws SVNException { // get base version if (currentFile.skip) { return; } currentFile.deltaProcessor = new SVNDeltaProcessor(); if (!currentFile.added) { currentFile.loadFile(context, repository, false, tmpFiles); } else { currentFile.startRevisionFile = getEmptyFile(); } File tmpDir = pureRemoteDiff ? getGlobalTmpDir() : context.getDb().getWCRootTempDir(target); currentFile.endRevisionFile = SVNFileUtil.createUniqueFile(tmpDir, SVNPathUtil.tail(path), ".tmp", false); tmpFiles.add(currentFile.endRevisionFile); currentFile.deltaProcessor.applyTextDelta(currentFile.startRevisionFile, currentFile.endRevisionFile, true); } public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException { if (currentFile.deltaProcessor != null) { return currentFile.deltaProcessor.textDeltaChunk(diffWindow); } return SVNFileUtil.DUMMY_OUT; } public void textDeltaEnd(String path) throws SVNException { if (currentFile.deltaProcessor != null) { String checksum = currentFile.deltaProcessor.textDeltaEnd(); currentFile.resultMd5Checksum = SvnChecksum.fromString("$md5 $" + checksum); } } public void targetRevision(long revision) throws SVNException { this.targetRevision = revision; } public void openRoot(long revision) throws SVNException { DirBaton baton = makeDirBaton("", null, false); baton.wcPath = target; baton.loadProperties(repository, "", revision); this.currentDir = baton; } public void deleteEntry(String path, long revision) throws SVNException { if (currentDir.skip || currentDir.skipChildren || currentDir.treeConflicted) { return; } SVNNodeKind kind = repository.checkPath(path, this.revision); SVNEventAction action = SVNEventAction.SKIP; currentResult.contentState = SVNStatusType.INAPPLICABLE; if (kind == SVNNodeKind.FILE) { FileBaton b = makeFileBaton(path, false); b.loadFile(context, repository, false, tmpFiles); b.endRevisionFile = getEmptyFile(); String[] mTypes = b.getMimeTypes(); diffCallback.fileDeleted(currentResult, b.wcPath, b.startRevisionFile, b.endRevisionFile, mTypes[0], mTypes[1], b.pristineProps); } else if (kind == SVNNodeKind.DIR) { diffCallback.dirDeleted(currentResult, SVNFileUtil.createFilePath(target, path)); if (walkDeletedDirs) { diffDeletedDir(path, this.revision, repository); } } if (currentResult.contentState != SVNStatusType.MISSING && currentResult.contentState != SVNStatusType.OBSTRUCTED && !currentResult.treeConflicted) { action = SVNEventAction.UPDATE_DELETE; } if (eventHandler != null) { final DeletedPath dp = new DeletedPath(); dp.action = currentResult.treeConflicted ? SVNEventAction.TREE_CONFLICT : action; dp.kind = kind; dp.state = currentResult.contentState; deletedPaths.put(SVNFileUtil.createFilePath(target, path), dp); } } private void diffDeletedDir(String path, long revision, SVNRepository repository) throws SVNException { context.checkCancelled(); Collection<SVNDirEntry> entries = repository.getDir(path, revision, null, SVNDirEntry.DIRENT_KIND, (Collection<SVNDirEntry>) null); for (SVNDirEntry entry : entries) { if (entry.getName() == null || "".equals(entry.getName())) { continue; } String entryPath = SVNPathUtil.append(path, entry.getName()); if (entry.getKind() == SVNNodeKind.FILE) { FileBaton fb = makeFileBaton(entryPath, false); fb.loadFile(context, repository, false, tmpFiles); File emptyFile = getEmptyFile(); String[] mTypes = fb.getMimeTypes(); diffCallback.fileDeleted(null, fb.wcPath, fb.startRevisionFile, emptyFile, mTypes[0], mTypes[1], fb.pristineProps); } else if (entry.getKind() == SVNNodeKind.DIR) { diffDeletedDir(entryPath, revision, repository); } } } private File getEmptyFile() throws SVNException { if (emptyFile == null) { File tmpDir = pureRemoteDiff ? getGlobalTmpDir() : context.getDb().getWCRootTempDir(target); emptyFile = SVNFileUtil.createUniqueFile(tmpDir, "empty", ".tmp", false); tmpFiles.add(emptyFile); } return emptyFile; } public void absentDir(String path) throws SVNException { File file = SVNFileUtil.createFilePath(currentDir.wcPath, SVNPathUtil.tail(path)); SVNEvent event = SVNEventFactory.createSVNEvent(file, SVNNodeKind.DIR, null, -1, SVNStatusType.MISSING, SVNStatusType.MISSING, SVNStatusType.LOCK_INAPPLICABLE, SVNEventAction.SKIP, null, null, null, null); handleEvent(event); } public void absentFile(String path) throws SVNException { File file = SVNFileUtil.createFilePath(currentDir.wcPath, SVNPathUtil.tail(path)); SVNEvent event = SVNEventFactory.createSVNEvent(file, SVNNodeKind.FILE, null, -1, SVNStatusType.MISSING, SVNStatusType.MISSING, SVNStatusType.LOCK_INAPPLICABLE, SVNEventAction.SKIP, null, null, null, null); handleEvent(event); } public void addDir(String path, String copyFromPath, long copyFromRevision) throws SVNException { DirBaton db = makeDirBaton(path, currentDir, true); db.pristineProperties = new SVNProperties(); DirBaton pb = currentDir; currentDir = db; if (pb.skip || pb.skipChildren || pb.treeConflicted) { currentDir.skip = true; return; } diffCallback.dirAdded(currentResult.reset(), db.wcPath, targetRevision, copyFromPath, copyFromRevision); db.skip = currentResult.skip; db.skipChildren = currentResult.skipChildren; db.treeConflicted = currentResult.treeConflicted; SVNNodeKind kind = SVNNodeKind.DIR; SVNEventAction action = null; DeletedPath dp = deletedPaths.get(db.wcPath); if (dp != null) { currentResult.contentState = dp.state; kind = dp.kind; deletedPaths.remove(db.wcPath); } if (db.treeConflicted) { action = SVNEventAction.TREE_CONFLICT; } else if (dp != null) { if (dp.action == SVNEventAction.UPDATE_DELETE) { action = SVNEventAction.UPDATE_REPLACE; } else { action = dp.action; } } else if (currentResult.contentState == SVNStatusType.MISSING || currentResult.contentState == SVNStatusType.OBSTRUCTED) { action = SVNEventAction.SKIP; } else { action = SVNEventAction.UPDATE_ADD; } SVNEvent event = SVNEventFactory.createSVNEvent(db.wcPath, kind, null, -1, currentResult.contentState, currentResult.contentState, SVNStatusType.LOCK_INAPPLICABLE, action, action, null, null, null); handleEvent(event); } public void openDir(String path, long revision) throws SVNException { DirBaton db = makeDirBaton(path, currentDir, false); db.pristineProperties = new SVNProperties(); DirBaton pb = currentDir; currentDir = db; if (pb.skip || pb.skipChildren || pb.treeConflicted) { currentDir.skip = true; return; } db.loadProperties(repository, path, revision); diffCallback.dirOpened(currentResult.reset(), db.wcPath, revision); db.skip = currentResult.skip; db.skipChildren = currentResult.skipChildren; db.treeConflicted = currentResult.treeConflicted; } public void changeDirProperty(String name, SVNPropertyValue value) throws SVNException { if (currentDir.skip) { return; } currentDir.propChanges.put(name, value); } public void closeDir() throws SVNException { boolean skipped = false; currentResult.reset(); currentResult.contentState = SVNStatusType.UNKNOWN; currentResult.propState = SVNStatusType.UNKNOWN; DirBaton b = currentDir; if (b.skip) { currentDir = b.parent; return; } if (!b.added && b.propChanges.size() > 0) { removeNonPropChanges(b.pristineProperties, b.propChanges); } if (b.propChanges.size() > 0) { diffCallback.dirPropsChanged(currentResult, b.wcPath, b.added, b.propChanges, b.pristineProperties); if (currentResult.propState == SVNStatusType.OBSTRUCTED || currentResult.propState == SVNStatusType.MISSING) { currentResult.contentState = currentResult.propState; skipped = true; } } diffCallback.dirClosed(null, b.wcPath, b.added); if (!skipped && !b.added) { for (File d : deletedPaths.keySet()) { DeletedPath dp = deletedPaths.get(d); SVNEvent event = SVNEventFactory.createSVNEvent(d, dp.kind, null, -1, dp.state, dp.state, SVNStatusType.LOCK_INAPPLICABLE, dp.action, dp.action, null, null, null); handleEvent(event); } deletedPaths.clear(); } if (!b.added) { SVNEventAction action = null; if (b.treeConflicted) { action = SVNEventAction.TREE_CONFLICT; } else if (skipped) { action = SVNEventAction.SKIP; } else { action = SVNEventAction.UPDATE_UPDATE; } SVNEvent event = SVNEventFactory.createSVNEvent(b.wcPath, SVNNodeKind.DIR, null, -1, currentResult.contentState, currentResult.propState, SVNStatusType.LOCK_INAPPLICABLE, action, action, null, null, null); handleEvent(event); } currentDir = b.parent; } public void addFile(String path, String copyFromPath, long copyFromRevision) throws SVNException { FileBaton fb = makeFileBaton(path, true); currentFile = fb; if (currentDir.skip || currentDir.skipChildren || currentDir.treeConflicted) { fb.skip = true; return; } fb.pristineProps = new SVNProperties(); } public void openFile(String path, long revision) throws SVNException { FileBaton fb = makeFileBaton(path, false); currentFile = fb; if (currentDir.skip || currentDir.skipChildren || currentDir.treeConflicted) { fb.skip = true; return; } fb.baseRevision = revision; diffCallback.fileOpened(currentResult.reset(), fb.wcPath, revision); fb.treeConflicted = currentResult.treeConflicted; fb.skip = currentResult.skip; } public void changeFileProperty(String path, String propertyName, SVNPropertyValue propertyValue) throws SVNException { if (currentFile.skip) { return; } currentFile.propChanges.put(propertyName, propertyValue); } public void closeFile(String path, String textChecksum) throws SVNException { if (currentFile.skip) { currentFile = null; return; } FileBaton b = currentFile; if (textChecksum != null) { SvnChecksum expected = SvnChecksum.fromString("$md5 $" + textChecksum); if (expected != null && b.resultMd5Checksum != null && !expected.equals(b.resultMd5Checksum)) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH, "Checksum mismatch for ''{0}''", b.repoPath); SVNErrorManager.error(err, SVNLogType.WC); } } if (!b.added && b.propChanges.size() > 0) { if (b.pristineProps == null) { b.loadFile(context, repository, true, tmpFiles); } removeNonPropChanges(b.pristineProps, b.propChanges); } if (b.endRevisionFile != null || b.propChanges.size() > 0) { String[] mTypes = b.getMimeTypes(); if (b.added) { diffCallback.fileAdded(currentResult.reset(), b.wcPath, b.endRevisionFile != null ? b.startRevisionFile : null, b.endRevisionFile, 0, targetRevision, mTypes[0], mTypes[1], null, -1, b.propChanges, b.pristineProps); b.treeConflicted = currentResult.treeConflicted; } else { diffCallback.fileChanged(currentResult.reset(), b.wcPath, b.endRevisionFile != null ? b.startRevisionFile : null, b.endRevisionFile, revision, targetRevision, mTypes[0], mTypes[1], b.propChanges, b.pristineProps); b.treeConflicted = currentResult.treeConflicted; } } SVNNodeKind kind = SVNNodeKind.FILE; SVNEventAction action = null; DeletedPath dp = deletedPaths.get(b.wcPath); if (dp != null) { deletedPaths.remove(b.wcPath); kind = dp.kind; currentResult.contentState = dp.state; currentResult.propState = dp.state; } SVNEventAction expectedAction = null; if (b.treeConflicted) { action = SVNEventAction.TREE_CONFLICT; } else if (dp != null) { if (dp.action == SVNEventAction.UPDATE_DELETE && b.added) { action = SVNEventAction.UPDATE_REPLACE; } else { action = dp.action; } } else if (currentResult.contentState == SVNStatusType.OBSTRUCTED || currentResult.contentState == SVNStatusType.MISSING) { action = SVNEventAction.SKIP; expectedAction = b.added ? SVNEventAction.UPDATE_ADD : SVNEventAction.UPDATE_UPDATE; } else if (b.added) { action = SVNEventAction.UPDATE_ADD; } else { action = SVNEventAction.UPDATE_UPDATE; } if (expectedAction == null) { expectedAction = action; } SVNEvent event = SVNEventFactory.createSVNEvent(b.wcPath, kind, null, -1, currentResult.contentState, currentResult.propState, SVNStatusType.LOCK_INAPPLICABLE, action, expectedAction, null, null, null); handleEvent(event); } private void removeNonPropChanges(SVNProperties pristineProps, SVNProperties propChanges) { Set<String> removed = new HashSet<String>(); for (String propertyName : propChanges.nameSet()) { SVNPropertyValue newValue = propChanges.getSVNPropertyValue(propertyName); if (newValue != null) { SVNPropertyValue oldValue = pristineProps.getSVNPropertyValue(propertyName); if (oldValue != null && oldValue.equals(newValue)) { removed.add(propertyName); } } } for (String name : removed) { propChanges.remove(name); } } public SVNCommitInfo closeEdit() throws SVNException { cleanup(); return null; } public void cleanup() { for (File tmpFile : tmpFiles) { try { SVNFileUtil.deleteFile(tmpFile); } catch (SVNException ignore) { } } if (globalTmpDir != null) { SVNFileUtil.deleteAll(globalTmpDir, true); } } public void abortEdit() throws SVNException { } }