/* * ==================================================================== * 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.wc; import de.regnis.q.sequence.line.diff.QDiffGenerator; import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; import de.regnis.q.sequence.line.diff.QDiffManager; import de.regnis.q.sequence.line.diff.QDiffUniGenerator; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNMergeRangeList; 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.SVNHashMap; import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; import org.tmatesoft.svn.core.internal.util.SVNPathUtil; import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.util.SVNLogType; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.TreeMap; /** * <b>DefaultSVNDiffGenerator</b> is a default implementation of * <b>ISVNDiffGenerator</b>. * <p> * By default, if there's no any specified implementation of the diff generator's * interface, SVNKit uses this default implementation. To set a custom * diff driver use {@link SVNDiffClient#setDiffGenerator(ISVNDiffGenerator) setDiffGenerator()}. * * @version 1.3 * @since 1.2 * @author TMate Software Ltd. */ public class DefaultSVNDiffGenerator implements ISVNDiffGenerator { protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; protected static final String HEADER_SEPARATOR = "==================================================================="; protected static final String WC_REVISION_LABEL = "(working copy)"; protected static final InputStream EMPTY_FILE_IS = SVNFileUtil.DUMMY_IN; private boolean myIsForcedBinaryDiff; private String myAnchorPath1; private String myAnchorPath2; private ISVNOptions myOptions; private String myEncoding; private byte[] myEOL; private boolean myIsDiffDeleted; private boolean myIsDiffAdded; private boolean myIsDiffCopied; private File myBasePath; private boolean myIsDiffUnversioned; private SVNDiffOptions myDiffOptions; private Collection myRawDiffOptions; private String myDiffCommand; private boolean myIsUseAbsolutePaths; /** * Constructs a <b>DefaultSVNDiffGenerator</b>. * */ public DefaultSVNDiffGenerator() { myAnchorPath1 = ""; myAnchorPath2 = ""; setDiffDeleted(true); setDiffAdded(true); } /** * Initializes this generator with old and new diff anchor paths. * * @param anchorPath1 an old path/URL * @param anchorPath2 a new path/URL */ public void init(String anchorPath1, String anchorPath2) { myAnchorPath1 = anchorPath1.replace(File.separatorChar, '/'); myAnchorPath2 = anchorPath2.replace(File.separatorChar, '/'); } /** * Sets diff options containing diff rules. * * @param options diff options */ public void setDiffOptions(SVNDiffOptions options) { myDiffOptions = options; } /** * Sets a collection of raw (<code>String</code>) diff options. * * @param options raw options */ public void setRawDiffOptions(Collection options) { myRawDiffOptions = options; } /** * Sets global run-time options. * * @param options options implementation */ public void setOptions(ISVNOptions options){ myOptions = options; } /** * Sets an external diff program for producing the difference between files. * * @param command external diff program */ public void setExternalDiffCommand(String command) { myDiffCommand = command; } /** * Sets the base path that must be stripped from the front of the paths of compared files. * If <code>basePath</code> is not <span class="javakeyword">null</span> but is not a parent path of * the target, this will lead to an error during diff. * * <p/> * Note: <code>basePath</code> doesn't affect the path index generated by external diff programs. * * @param basePath common parent path to strip off the displayed paths */ public void setBasePath(File basePath) { myBasePath = basePath; } /** * Controls whether error is reported on failure to compute relative display path, * or absolute path is used instead. * * @param fallback true to make generator use absolute path when relative path could not * be computed. */ public void setFallbackToAbsolutePath(boolean fallback) { myIsUseAbsolutePaths = fallback; } /** * Enables or disables diffing deleted files. * * @param isDiffDeleted */ public void setDiffDeleted(boolean isDiffDeleted) { myIsDiffDeleted = isDiffDeleted; } /** * Tells whether deleted files must be diffed also. * * @return <span class="javakeyword">true</span> if deleted files must be diffed also */ public boolean isDiffDeleted() { return myIsDiffDeleted; } /** * Enables or disables diffing added files. * * @param isDiffAdded */ public void setDiffAdded(boolean isDiffAdded) { myIsDiffAdded = isDiffAdded; } /** * Tells whether added files must be diffed also. * * @return <span class="javakeyword">true</span> if added files must be diffed also */ public boolean isDiffAdded() { return myIsDiffAdded; } /** * Enables or disables copied files diffing. * * @param isDiffCopied */ public void setDiffCopied(boolean isDiffCopied) { myIsDiffCopied = isDiffCopied; } /** * Tells whether deleted files must be diffed also. * * @return <span class="javakeyword">true</span> if copied files must be diffed also */ public boolean isDiffCopied() { return myIsDiffCopied; } /** * Gets the diff options that are used by this generator. * Creates a new one if none was used before. * * @return diff options */ public SVNDiffOptions getDiffOptions() { if (myDiffOptions == null) { myDiffOptions = new SVNDiffOptions(); } return myDiffOptions; } protected String getDisplayPath(String path) throws SVNException { if (myBasePath == null) { return path; } if (path == null) { path = ""; } if (SVNPathUtil.isURL(path)) { return path; } // treat as file path. String basePath = myBasePath.getAbsolutePath().replace(File.separatorChar, '/'); path = new File(path).getAbsolutePath().replace(File.separatorChar, '/'); if (path.equals(basePath)) { return "."; } String relativePath = SVNPathUtil.getPathAsChild(basePath, path); if (relativePath == null) { if (myIsUseAbsolutePaths) { return path; } createBadRelativePathError(path); } if (relativePath.startsWith("./")) { relativePath = relativePath.substring("./".length()); } return relativePath; } /** * Sets whether binary files diff must be forced or not. * * @param forced whether to force binary diff or not */ public void setForcedBinaryDiff(boolean forced) { myIsForcedBinaryDiff = forced; } /** * Tells if this generator forced binary files diff. * * @return <span class="javakeyword">true</span> if forces; otherwise <span class="javakeyword">false</span> */ public boolean isForcedBinaryDiff() { return myIsForcedBinaryDiff; } /** * Produces properties difference and writes it to <code>result</code>. * * @param path * @param baseProps * @param diff * @param result * @throws SVNException in the following cases: * <ul> * <li/>exception with {@link SVNErrorCode#IO_ERROR} error code - if an I\O error occurred * </ul> */ public void displayPropDiff(String path, SVNProperties baseProps, SVNProperties diff, OutputStream result) throws SVNException { baseProps = baseProps != null ? baseProps : new SVNProperties(); diff = diff != null ? diff : new SVNProperties(); for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext();) { String name = (String) changedPropNames.next(); SVNPropertyValue originalValue = baseProps.getSVNPropertyValue(name); SVNPropertyValue newValue = diff.getSVNPropertyValue(name); if ((originalValue != null && originalValue.equals(newValue)) || (originalValue == null && newValue == null)) { changedPropNames.remove(); } } if (diff.isEmpty()) { return; } path = getDisplayPath(path); ByteArrayOutputStream bos = new ByteArrayOutputStream(); diff = new SVNProperties(diff); try { bos.write(getEOL()); bos.write(("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path)).getBytes(getEncoding())); bos.write(getEOL()); bos.write(PROPERTIES_SEPARATOR.getBytes(getEncoding())); bos.write(getEOL()); for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext();) { String name = (String) changedPropNames.next(); SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; SVNPropertyValue newValue = diff.getSVNPropertyValue(name); String headerFormat = null; if (originalValue == null) { headerFormat = "Added: "; } else if (newValue == null) { headerFormat = "Deleted: "; } else { headerFormat = "Modified: "; } bos.write((headerFormat + name).getBytes(getEncoding())); bos.write(getEOL()); if (SVNProperty.MERGE_INFO.equals(name)) { displayMergeInfoDiff(bos, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); continue; } if (originalValue != null) { bos.write(" - ".getBytes(getEncoding())); bos.write(getPropertyAsBytes(originalValue, getEncoding())); bos.write(getEOL()); } if (newValue != null) { bos.write(" + ".getBytes(getEncoding())); bos.write(getPropertyAsBytes(newValue, getEncoding())); bos.write(getEOL()); } } bos.write(getEOL()); } catch (IOException e) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage()); SVNErrorManager.error(err, e, SVNLogType.DEFAULT); } finally { try { bos.close(); bos.writeTo(result); } catch (IOException e) { } } } private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding){ if (value == null){ return null; } if (value.isString()){ try { return value.getString().getBytes(encoding); } catch (UnsupportedEncodingException e) { return value.getString().getBytes(); } } return value.getBytes(); } protected File getBasePath() { return myBasePath; } /** * Writes the difference between <code>file1</code> and <code>file2</code> as they are seen in * <code>rev1</code> and <code>rev2</code> to <code>result</code>. * * @param path * @param file1 * @param file2 * @param rev1 * @param rev2 * @param mimeType1 * @param mimeType2 * @param result * @throws SVNException in the following cases: * <ul> * <li/>exception with {@link SVNErrorCode#EXTERNAL_PROGRAM} error code - if an external diff program * exited with an error code value different from <code>0</code> and <code>1</code> * <li/>exception with {@link SVNErrorCode#IO_ERROR} error code - if an I\O error occurred * </ul> */ public void displayFileDiff(String path, File file1, File file2, String rev1, String rev2, String mimeType1, String mimeType2, OutputStream result) throws SVNException { path = getDisplayPath(path); // if anchor1 is the same as anchor2 just use path. // if anchor1 differs from anchor2 => // condence anchors (get common root and remainings). int i = 0; for(; i < myAnchorPath1.length() && i < myAnchorPath2.length() && myAnchorPath1.charAt(i) == myAnchorPath2.charAt(i); i++) {} if (i < myAnchorPath1.length() || i < myAnchorPath2.length()) { if (i == myAnchorPath1.length()) { i = myAnchorPath1.length() - 1; } for(; i > 0 && myAnchorPath1.charAt(i) != '/'; i--) {} } String p1 = myAnchorPath1.substring(i) ; String p2 = myAnchorPath2.substring(i); if (p1.length() == 0) { p1 = path; } else if (p1.charAt(0) == '/') { p1 = path + "\t(..." + p1 + ")"; } else { p1 = path + "\t(.../" + p1 + ")"; } if (p2.length() == 0) { p2 = path; } else if (p2.charAt(0) == '/') { p2 = path + "\t(..." + p2 + ")"; } else { p2 = path + "\t(.../" + p2 + ")"; } String label1 = getLabel(p1, rev1); String label2 = getLabel(p2, rev2); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { if (displayHeader(bos, path, file2 == null)) { bos.close(); bos.writeTo(result); return; } if (isHeaderForced(file1, file2)) { bos.writeTo(result); bos.reset(); } } catch (IOException e) { try { bos.close(); bos.writeTo(result); } catch (IOException inner) { } SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage()); SVNErrorManager.error(err, e, SVNLogType.DEFAULT); } if (!isForcedBinaryDiff() && (SVNProperty.isBinaryMimeType(mimeType1) || SVNProperty.isBinaryMimeType(mimeType2))) { try { displayBinary(bos, mimeType1, mimeType2); } catch (IOException e) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage()); SVNErrorManager.error(err, e, SVNLogType.DEFAULT); } finally { try { bos.close(); bos.writeTo(result); } catch (IOException e) { } } return; } if (file1 == file2 && file1 == null) { try { bos.close(); bos.writeTo(result); } catch (IOException e) { } return; } final String diffCommand = getExternalDiffCommand(); if (diffCommand != null) { try { bos.close(); bos.writeTo(result); } catch (IOException e) { } Collection args = new LinkedList(); File diffCommandFile = new File(diffCommand); args.add(diffCommandFile.getAbsolutePath().replace(File.separatorChar, '/')); if (myRawDiffOptions != null) { args.addAll(myRawDiffOptions); } else { Collection diffOptions = getDiffOptions().toOptionsCollection(); args.addAll(diffOptions); args.add("-u"); } if (label1 != null) { args.add("-L"); args.add(label1); } if (label2 != null) { args.add("-L"); args.add(label2); } boolean tmpFile1 = false; boolean tmpFile2 = false; if (file1 == null) { file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); tmpFile1 = true; } if (file2 == null) { file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); tmpFile2 = true; } String currentDir = new File("").getAbsolutePath().replace(File.separatorChar, '/'); String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); if (file1Path.startsWith(currentDir)) { file1Path = file1Path.substring(currentDir.length()); file1Path = file1Path.startsWith("/") ? file1Path.substring(1) : file1Path; } if (file2Path.startsWith(currentDir)) { file2Path = file2Path.substring(currentDir.length()); file2Path = file2Path.startsWith("/") ? file2Path.substring(1) : file2Path; } args.add(file1Path); args.add(file2Path); try { final Writer writer = new OutputStreamWriter(result, getEncoding()); SVNFileUtil.execCommand((String[]) args.toArray(new String[args.size()]), true, new ISVNReturnValueCallback() { public void handleReturnValue(int returnValue) throws SVNException { if (returnValue != 0 && returnValue != 1) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, "''{0}'' returned {1}", new Object[] { diffCommand, String.valueOf(returnValue) }); SVNErrorManager.error(err, SVNLogType.DEFAULT); } } public void handleChar(char ch) throws SVNException { try { writer.write(ch); } catch (IOException ioe) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); } } public boolean isHandleProgramOutput() { return true; } }); writer.flush(); } catch (IOException ioe) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); } finally { try { if (tmpFile1) { SVNFileUtil.deleteFile(file1); } if (tmpFile2) { SVNFileUtil.deleteFile(file2); } } catch (SVNException e) { // skip } } return; } // put header fields. try { displayHeaderFields(bos, label1, label2); } catch (IOException e) { try { bos.close(); bos.writeTo(result); } catch (IOException inner) { } SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage()); SVNErrorManager.error(err, e, SVNLogType.DEFAULT); } String header; try { bos.close(); header = bos.toString(getEncoding()); } catch (IOException inner) { header = ""; } RandomAccessFile is1 = null; RandomAccessFile is2 = null; try { is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); QDiffUniGenerator.setup(); Map properties = new SVNHashMap(); properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); if (getDiffOptions().isIgnoreAllWhitespace()) { properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); } QDiffGenerator generator = new QDiffUniGenerator(properties, header); Writer writer = new OutputStreamWriter(result, getEncoding()); QDiffManager.generateTextDiff(is1, is2, getEncoding(), writer, generator); writer.flush(); } catch (IOException e) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); SVNErrorManager.error(err, e, SVNLogType.DEFAULT); } finally { SVNFileUtil.closeFile(is1); SVNFileUtil.closeFile(is2); } } /** * Sets the encoding to use for diff output. * @param encoding charset name */ public void setEncoding(String encoding) { myEncoding = encoding; } /** * Returns the encoding used for diff output. * * @return charset name */ public String getEncoding() { if (hasEncoding()) { return myEncoding; } return getOptions().getNativeCharset(); } /** * Says whether this generator is using any special (non-native) * charset for outputting diffs. * * @return <span class="javakeyword">true</span> if yes; * otherwise <span class="javakeyword">false</span> */ public boolean hasEncoding() { return myEncoding != null; } /** * Says whether this generator is using any special (non-native) * EOL bytes for outputting diffs. * * @return <span class="javakeyword">true</span> if yes; * otherwise <span class="javakeyword">false</span> */ public boolean hasEOL() { return myEOL != null; } /** * Returns the encoding specified by svnkit.global-charset option * of the global configuration. * * @return global charset name */ public String getGlobalEncoding() { if (getOptions() instanceof DefaultSVNOptions) { DefaultSVNOptions defaultOptions = (DefaultSVNOptions) getOptions(); return defaultOptions.getGlobalCharset(); } return null; } /** * Sets the EOL bytes to use in diff output. * * @param eol EOL bytes */ public void setEOL(byte[] eol){ myEOL = eol; } /** * Returns the EOL marker bytes being in use. * If no EOL bytes were provided, uses {@link ISVNOptions#getNativeEOL() native EOL} fetched from * the options. * * @return EOL bytes */ public byte[] getEOL(){ if (myEOL == null){ myEOL = getOptions().getNativeEOL(); } return myEOL; } /** * Creates a temporary directory for diff files. * * @return returns the temp directory * @throws SVNException */ public File createTempDirectory() throws SVNException { return SVNFileUtil.createTempDirectory("diff"); } /** * Says if unversioned files are also diffed or ignored. * * <p> * By default unversioned files are ignored. * * @return <span class="javakeyword">true</span> if diffed, * <span class="javakeyword">false</span> if ignored * @see #setDiffUnversioned(boolean) * */ public boolean isDiffUnversioned() { return myIsDiffUnversioned; } /** * Includes or not unversioned files into diff processing. * * <p> * If a diff operation is invoked on a versioned directory and * <code>diffUnversioned</code> is <span class="javakeyword">true</span> * then all unversioned files that may be met in the directory will * be processed as added. Otherwise if <code>diffUnversioned</code> * is <span class="javakeyword">false</span> such files are ignored. * * <p> * By default unversioned files are ignored. * * @param diffUnversioned controls whether to diff unversioned files * or not * @see #isDiffUnversioned() */ public void setDiffUnversioned(boolean diffUnversioned) { myIsDiffUnversioned = diffUnversioned; } /** * Does nothing. * * @param path a directory path * @param rev1 the first diff revision * @param rev2 the second diff revision * @throws SVNException */ public void displayDeletedDirectory(String path, String rev1, String rev2) throws SVNException { // not implemented. } /** * Does nothing. * * @param path a directory path * @param rev1 the first diff revision * @param rev2 the second diff revision * @throws SVNException */ public void displayAddedDirectory(String path, String rev1, String rev2) throws SVNException { // not implemented. } protected String getExternalDiffCommand() { if (myDiffCommand != null) { return myDiffCommand; } if (myOptions instanceof DefaultSVNOptions) { return ((DefaultSVNOptions) myOptions).getDiffCommand(); } return null; } protected ISVNOptions getOptions(){ if (myOptions == null){ myOptions = new DefaultSVNOptions(); } return myOptions; } protected void displayBinary(OutputStream os, String mimeType1, String mimeType2) throws IOException { os.write("Cannot display: file marked as a binary type.".getBytes(getEncoding())); os.write(getEOL()); if (SVNProperty.isBinaryMimeType(mimeType1) && !SVNProperty.isBinaryMimeType(mimeType2)) { os.write("svn:mime-type = ".getBytes(getEncoding())); os.write(mimeType1.getBytes(getEncoding())); os.write(getEOL()); } else if (!SVNProperty.isBinaryMimeType(mimeType1) && SVNProperty.isBinaryMimeType(mimeType2)) { os.write("svn:mime-type = ".getBytes(getEncoding())); os.write(mimeType2.getBytes(getEncoding())); os.write(getEOL()); } else if (SVNProperty.isBinaryMimeType(mimeType1) && SVNProperty.isBinaryMimeType(mimeType2)) { if (mimeType1.equals(mimeType2)) { os.write("svn:mime-type = ".getBytes(getEncoding())); os.write(mimeType2.getBytes(getEncoding())); os.write(getEOL()); } else { os.write("svn:mime-type = (".getBytes(getEncoding())); os.write(mimeType1.getBytes(getEncoding())); os.write(", ".getBytes(getEncoding())); os.write(mimeType2.getBytes(getEncoding())); os.write(")".getBytes(getEncoding())); os.write(getEOL()); } } } protected boolean displayHeader(OutputStream os, String path, boolean deleted) throws IOException { if (deleted && !isDiffDeleted()) { os.write("Index: ".getBytes(getEncoding())); os.write(path.getBytes(getEncoding())); os.write(" (deleted)".getBytes(getEncoding())); os.write(getEOL()); os.write(HEADER_SEPARATOR.getBytes(getEncoding())); os.write(getEOL()); return true; } os.write("Index: ".getBytes(getEncoding())); os.write(path.getBytes(getEncoding())); os.write(getEOL()); os.write(HEADER_SEPARATOR.getBytes(getEncoding())); os.write(getEOL()); return false; } protected void displayHeaderFields(OutputStream os, String label1, String label2) throws IOException { os.write("--- ".getBytes(getEncoding())); os.write(label1.getBytes(getEncoding())); os.write(getEOL()); os.write("+++ ".getBytes(getEncoding())); os.write(label2.getBytes(getEncoding())); os.write(getEOL()); } protected boolean isHeaderForced(File file1, File file2) { return (file1 == null && file2 != null); } protected boolean useLocalFileSeparatorChar() { return true; } protected String getLabel(String path, String revToken) { revToken = revToken == null ? WC_REVISION_LABEL : revToken; return path + "\t" + revToken; } private void displayMergeInfoDiff(ByteArrayOutputStream baos, String oldValue, String newValue) throws SVNException, IOException { Map oldMergeInfo = null; Map newMergeInfo = null; if (oldValue != null) { oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); } if (newValue != null) { newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); } Map deleted = new TreeMap(); Map added = new TreeMap(); SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); for (Iterator paths = deleted.keySet().iterator(); paths.hasNext();) { String path = (String) paths.next(); SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); baos.write((" Reverse-merged " + path + ":r").getBytes(getEncoding())); baos.write(rangeList.toString().getBytes(getEncoding())); baos.write(getEOL()); } for (Iterator paths = added.keySet().iterator(); paths.hasNext();) { String path = (String) paths.next(); SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); baos.write((" Merged " + path + ":r").getBytes(getEncoding())); baos.write(rangeList.toString().getBytes(getEncoding())); baos.write(getEOL()); } } private void createBadRelativePathError(String path) throws SVNException { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{1}''", new Object[] { path, myBasePath }); SVNErrorManager.error(err, SVNLogType.DEFAULT); } }