/* * ==================================================================== * Copyright (c) 2004-2010 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.patch; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.tmatesoft.svn.core.SVNDepth; 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.SVNProperty; import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; import org.tmatesoft.svn.core.internal.wc.SVNEventFactory; import org.tmatesoft.svn.core.internal.wc.SVNFileType; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.core.internal.wc.SVNStatusUtil; import org.tmatesoft.svn.core.internal.wc.SVNWCManager; import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea; 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.io.SVNRepository; 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.SVNStatus; import org.tmatesoft.svn.core.wc.SVNStatusType; import org.tmatesoft.svn.util.SVNLogType; /** * @version 1.3 * @author TMate Software Ltd. */ public class SVNPatchTarget { private static final int MAX_FUZZ = 2; private SVNPatch patch; private List lines = new ArrayList(); private List hunks = new ArrayList();; private boolean localMods; private boolean executable; private boolean skipped; private String eolStr; private Map keywords; private String eolStyle; private SVNNodeKind kind; private int currentLine; private boolean modified; private boolean hadRejects; private boolean deleted; private boolean eof; private boolean added; private File absPath; private File relPath; private File canonPathFromPatchfile; private RandomAccessFile file; private SVNPatchFileStream stream; private File patchedPath; private OutputStream patchedRaw; private OutputStream patched; private File rejectPath; private SVNPatchFileStream reject; private boolean parentDirExists; private SVNPatchTarget() { } public boolean isLocalMods() { return localMods; } public String getEolStr() { return eolStr; } public Map getKeywords() { return keywords; } public String getEolStyle() { return eolStyle; } public RandomAccessFile getFile() { return file; } public OutputStream getPatchedRaw() { return patchedRaw; } public File getCanonPathFromPatchfile() { return canonPathFromPatchfile; } public SVNPatch getPatch() { return patch; } public int getCurrentLine() { return currentLine; } public boolean isModified() { return modified; } public boolean isEof() { return eof; } public List getLines() { return lines; } public boolean isSkipped() { return skipped; } public List getHunks() { return hunks; } public SVNNodeKind getKind() { return kind; } public SVNPatchFileStream getStream() { return stream; } public OutputStream getPatched() { return patched; } public SVNPatchFileStream getReject() { return reject; } public File getPatchedPath() { return patchedPath; } public boolean isAdded() { return added; } public boolean isDeleted() { return deleted; } public boolean isExecutable() { return executable; } public File getRejectPath() { return rejectPath; } public File getAbsPath() { return absPath; } public File getRelPath() { return relPath; } public boolean isHadRejects() { return hadRejects; } public boolean isParentDirExists() { return parentDirExists; } /** * Attempt to initialize a patch TARGET structure for a target file * described by PATCH. Use client context CTX to send notifiations and * retrieve WC_CTX. STRIP_COUNT specifies the number of leading path * components which should be stripped from target paths in the patch. Upon * success, return the patch target structure. Else, return NULL. * * @throws SVNException * @throws IOException */ public static SVNPatchTarget initPatchTarget(SVNPatch patch, File baseDir, int stripCount, SVNAdminArea wc) throws SVNException, IOException { final SVNPatchTarget new_target = new SVNPatchTarget(); new_target.resolveTargetPath(patch.getNewFilename(), baseDir, stripCount, wc); new_target.localMods = false; new_target.executable = false; if (!new_target.skipped) { final String nativeEOLMarker = SVNFileUtil.getNativeEOLMarker(wc.getWCAccess().getOptions()); new_target.eolStr = nativeEOLMarker; new_target.keywords = null; new_target.eolStyle = null; if (new_target.kind == SVNNodeKind.FILE) { /* Open the file. */ new_target.file = SVNFileUtil.openRAFileForReading(new_target.absPath); /* Create a stream to read from the target. */ new_target.stream = SVNPatchFileStream.openReadOnly(new_target.absPath); /* Handle svn:keyword and svn:eol-style properties. */ SVNVersionedProperties props = wc.getProperties(new_target.absPath.getAbsolutePath()); String keywords_val = props.getStringPropertyValue(SVNProperty.KEYWORDS); if (null != keywords_val) { SVNEntry entry = wc.getEntry(new_target.absPath.getAbsolutePath(), false); long changed_rev = entry.getRevision(); String author = entry.getAuthor(); String changed_date = entry.getCommittedDate(); String url = entry.getURL(); String repositoryRoot = entry.getRepositoryRoot(); String rev_str = Long.toString(changed_rev); new_target.keywords = SVNTranslator.computeKeywords(keywords_val, url, repositoryRoot, author, changed_date, rev_str, wc.getWCAccess().getOptions()); } String eol_style_val = props.getStringPropertyValue(SVNProperty.EOL_STYLE); if (null != eol_style_val) { new_target.eolStyle = new String(SVNTranslator.getEOL(eol_style_val, wc.getWCAccess().getOptions())); } else { /* Just use the first EOL sequence we can find in the file. */ new_target.eolStr = detectFileEOL(new_target.file); /* But don't enforce any particular EOL-style. */ new_target.eolStyle = null; } if (new_target.eolStyle == null) { /* * We couldn't figure out the target files's EOL scheme, * just use native EOL makers. */ new_target.eolStr = nativeEOLMarker; new_target.eolStyle = SVNProperty.EOL_STYLE_NATIVE; } /* Also check the file for local mods and the Xbit. */ new_target.localMods = wc.hasTextModifications(new_target.absPath.getAbsolutePath(), false); new_target.executable = SVNFileUtil.isExecutable(new_target.absPath); } /* * Create a temporary file to write the patched result to. Expand * keywords in the patched file. */ new_target.patchedPath = SVNFileUtil.createTempFile("", null); new_target.patchedRaw = SVNFileUtil.openFileForWriting(new_target.patchedPath); new_target.patched = SVNTranslator.getTranslatingOutputStream(new_target.patchedRaw, null, new_target.eolStr.getBytes(), new_target.eolStyle != null, new_target.keywords, true); /* * We'll also need a stream to write rejected hunks to. We don't * expand keywords, nor normalise line-endings, in reject files. */ new_target.rejectPath = SVNFileUtil.createTempFile("", null); new_target.reject = SVNPatchFileStream.openForWrite(new_target.rejectPath); /* The reject stream needs a diff header. */ String diff_header = "--- " + new_target.canonPathFromPatchfile + nativeEOLMarker + "+++ " + new_target.canonPathFromPatchfile + nativeEOLMarker; new_target.reject.write(diff_header); } new_target.patch = patch; new_target.currentLine = 1; new_target.modified = false; new_target.hadRejects = false; new_target.deleted = false; new_target.eof = false; new_target.lines = new ArrayList(); new_target.hunks = new ArrayList(); return new_target; } /** * Detect the EOL marker used in file and return it. If it cannot be * detected, return NULL. * * The file is searched starting at the current file cursor position. The * first EOL marker found will be returnd. So if the file has inconsistent * EOL markers, this won't be detected. * * Upon return, the original file cursor position is always preserved, even * if an error is thrown. */ private static String detectFileEOL(RandomAccessFile file) throws IOException { /* Remember original file offset. */ final long pos = file.getFilePointer(); try { final BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file.getFD())); final StringBuffer buf = new StringBuffer(); int b1; while ((b1 = stream.read()) > 0) { final char c1 = (char) b1; if (c1 == '\n' || c1 == '\r') { buf.append(c1); if (c1 == '\r') { final int b2 = stream.read(); if (b2 > 0) { final char c2 = (char) b2; if (c2 == '\n') { buf.append(c2); } } } return buf.toString(); } } } finally { file.seek(pos); } return null; } /** * Resolve the exact path for a patch TARGET at path PATH_FROM_PATCHFILE, * which is the path of the target as it appeared in the patch file. Put a * canonicalized version of PATH_FROM_PATCHFILE into * TARGET->CANON_PATH_FROM_PATCHFILE. WC_CTX is a context for the working * copy the patch is applied to. If possible, determine TARGET->WC_PATH, * TARGET->ABS_PATH, TARGET->KIND, TARGET->ADDED, and * TARGET->PARENT_DIR_EXISTS. Indicate in TARGET->SKIPPED whether the target * should be skipped. STRIP_COUNT specifies the number of leading path * components which should be stripped from target paths in the patch. * * @throws SVNException * @throws IOException */ private void resolveTargetPath(File pathFromPatchfile, File absWCPath, int stripCount, SVNAdminArea wc) throws SVNException, IOException { final SVNPatchTarget target = this; target.canonPathFromPatchfile = pathFromPatchfile; if ("".equals(target.canonPathFromPatchfile.getPath())) { /* An empty patch target path? What gives? Skip this. */ target.skipped = true; target.kind = SVNNodeKind.FILE; target.absPath = null; target.relPath = null; return; } File stripped_path; if (stripCount > 0) { stripped_path = stripPath(target.canonPathFromPatchfile, stripCount); } else { stripped_path = target.canonPathFromPatchfile; } if (stripped_path.isAbsolute()) { target.relPath = getChildPath(absWCPath, stripped_path); if (null == target.relPath) { /* * The target path is either outside of the working copy or it * is the working copy itself. Skip it. */ target.skipped = true; target.kind = SVNNodeKind.FILE; target.absPath = null; target.relPath = stripped_path; return; } } else { target.relPath = stripped_path; } /* * Make sure the path is secure to use. We want the target to be inside * of the working copy and not be fooled by symlinks it might contain. */ if (!isChildPath(absWCPath, target.relPath)) { /* The target path is outside of the working copy. Skip it. */ target.skipped = true; target.kind = SVNNodeKind.FILE; target.absPath = null; return; } target.absPath = new File(absWCPath, target.relPath.getPath()); /* Skip things we should not be messing with. */ final SVNStatus status = SVNStatusUtil.getStatus(target.absPath, wc.getWCAccess()); final SVNStatusType contentsStatus = status.getContentsStatus(); if (contentsStatus == SVNStatusType.STATUS_UNVERSIONED || contentsStatus == SVNStatusType.STATUS_IGNORED || contentsStatus == SVNStatusType.STATUS_OBSTRUCTED) { target.skipped = true; target.kind = SVNFileType.getNodeKind(SVNFileType.getType(target.absPath)); return; } target.kind = status.getKind(); if (SVNNodeKind.FILE.equals(target.kind)) { target.added = false; target.parentDirExists = true; } else if (SVNNodeKind.NONE.equals(target.kind) || SVNNodeKind.UNKNOWN.equals(target.kind)) { /* * The file is not there, that's fine. The patch might want to * create it. Check if the containing directory of the target * exists. We may need to create it later. */ target.added = true; File absDirname = target.absPath.getParentFile(); final SVNStatus status2 = SVNStatusUtil.getStatus(absDirname, wc.getWCAccess()); final SVNStatusType contentsStatus2 = status2.getContentsStatus(); SVNNodeKind kind = status2.getKind(); target.parentDirExists = (kind == SVNNodeKind.DIR && contentsStatus2 != SVNStatusType.STATUS_DELETED && contentsStatus2 != SVNStatusType.STATUS_MISSING); } else { target.skipped = true; } return; } public static boolean isChildPath(final File baseFile, final File file) throws IOException { if (null != file && baseFile != null) { final String basePath = baseFile.getCanonicalPath(); final File childFile = new File(basePath, file.getPath()); final String childPath = childFile.getCanonicalPath(); return childPath.startsWith(basePath) && childPath.length() > basePath.length(); } return false; } private File getChildPath(File basePath, File childPath) throws IOException { if (null != childPath && basePath != null) { final String base = basePath.getCanonicalPath(); final String child = childPath.getCanonicalPath(); if (child.startsWith(base) && child.length() > base.length()) { String substr = child.substring(base.length()); File subPath = new File(substr); if (!subPath.isAbsolute()) { return subPath; } if (substr.length() > 1) { substr = substr.substring(1); subPath = new File(substr); if (!subPath.isAbsolute()) { return subPath; } } } } return null; } public static File stripPath(File path, int stripCount) { if (path != null && stripCount > 0) { final String[] components = decomposePath(path); final StringBuffer buf = new StringBuffer(); if (stripCount <= components.length) { for (int i = stripCount; i < components.length; i++) { if (i > stripCount) { buf.append(File.separatorChar); } buf.append(components[i]); } return new File(buf.toString()); } } return path; } /** * Write the diff text of the hunk described by HI to the reject stream of * TARGET, and mark TARGET as having had rejects. * * @throws IOException * @throws SVNException */ public void rejectHunk(final SVNPatchHunkInfo hi) throws SVNException, IOException { final SVNPatchTarget target = this; final SVNPatchHunk hunk = hi.getHunk(); final StringBuffer hunk_header = new StringBuffer(); hunk_header.append("@@"); hunk_header.append(" -").append(hunk.getOriginal().getStart()).append(",").append(hunk.getOriginal().getLength()); hunk_header.append(" +").append(hunk.getModified().getStart()).append(",").append(hunk.getModified().getLength()); hunk_header.append(" ").append(target.eolStr); target.reject.write(hunk_header); boolean eof; final StringBuffer hunk_line = new StringBuffer(); final StringBuffer eol_str = new StringBuffer(); do { hunk_line.setLength(0); eol_str.setLength(0); eof = hunk.getDiffText().readLineWithEol(hunk_line, eol_str); if (!eof) { if (hunk_line.length() > 0) { target.reject.tryWrite(hunk_line); } if (eol_str.length() > 0) { target.reject.tryWrite(eol_str); } } } while (!eof); target.hadRejects = true; } /** * Write the modified text of hunk described by HI to the patched stream of * TARGET. * * @throws SVNException * @throws IOException */ public void applyHunk(final SVNPatchHunkInfo hi) throws SVNException, IOException { final SVNPatchTarget target = this; final SVNPatchHunk hunk = hi.getHunk(); if (target.kind == SVNNodeKind.FILE) { /* * Move forward to the hunk's line, copying data as we go. Also copy * leading lines of context which matched with fuzz. The target has * changed on the fuzzy-matched lines, so we should retain the * target's version of those lines. */ target.copyLinesToTarget(hi.getMatchedLine() + hi.getFuzz()); /* * Skip the target's version of the hunk. Don't skip trailing lines * which matched with fuzz. */ target.seekToLine(target.getCurrentLine() + hunk.getOriginal().getLength() - (2 * hi.getFuzz())); } /* * Write the hunk's version to the patched result. Don't write the lines * which matched with fuzz. */ long lines_read = 0; boolean eof = false; final StringBuffer hunk_line = new StringBuffer(); final StringBuffer eol_str = new StringBuffer(); do { eof = hunk.getModifiedText().readLineWithEol(hunk_line, eol_str); lines_read++; if (!eof && lines_read > hi.getFuzz() && lines_read <= hunk.getModified().getLength() - hi.getFuzz()) { if (hunk_line.length() > 0) { tryWrite(target.getPatched(), hunk_line); } if (eol_str.length() > 0) { tryWrite(target.getPatched(), eol_str); } } } while (!eof); } /** * Seek to the specified LINE in TARGET. Mark any lines not read before in * TARGET->LINES. * * @throws SVNException * @throws IOException */ public void seekToLine(int line) throws SVNException, IOException { if (line <= 0) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ASSERTION_FAIL, "Line to seek must be more than zero"); SVNErrorManager.error(err, Level.FINE, SVNLogType.WC); } final SVNPatchTarget target = this; if (line == target.currentLine) { return; } if (line <= target.lines.size()) { final Long mark = (Long) target.lines.get(line - 1); target.stream.setSeekPosition(mark.longValue()); target.currentLine = line; } else { final StringBuffer dummy = new StringBuffer(); while (target.currentLine < line) { target.readLine(dummy); } } } /** * Read a *LINE from TARGET. If the line has not been read before mark the * line in TARGET->LINES. * * @throws SVNException * @throws IOException */ public void readLine(final StringBuffer line) throws SVNException, IOException { final SVNPatchTarget target = this; if (target.eof) { return; } if (target.currentLine > target.lines.size() + 1) { SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ASSERTION_FAIL, "Lines reading isn't sequenced"); SVNErrorManager.error(err, Level.FINE, SVNLogType.WC); } if (target.currentLine == target.lines.size() + 1) { final Long mark = new Long(target.stream.getSeekPosition()); target.lines.add(mark); } final StringBuffer line_raw = new StringBuffer(); target.eof = target.stream.readLine(line_raw, target.eolStr); /* Contract keywords. */ final byte[] eol = target.eolStr.getBytes(); // TODO EOL bytes line.append(SVNTranslator.translateString(line_raw.toString(), eol, target.keywords, false, false)); target.currentLine++; } /** * Copy lines to the patched stream until the specified LINE has been * reached. Indicate in *EOF whether end-of-file was encountered while * reading from the target. If LINE is zero, copy lines until end-of-file * has been reached. * * @throws IOException */ public void copyLinesToTarget(int line) throws SVNException, IOException { final SVNPatchTarget target = this; while ((target.currentLine < line || line == 0) && !target.eof) { final StringBuffer target_line = new StringBuffer(); target.readLine(target_line); if (!target.eof) { target_line.append(target.eolStr); } tryWrite(target.patched, target_line); } } /** * Install a patched TARGET into the working copy at ABS_WC_PATH. Use client * context CTX to retrieve WC_CTX, and possibly doing notifications. If * DRY_RUN is TRUE, don't modify the working copy. * * @throws SVNException */ public void installPatchedTarget(File absWCPath, boolean dryRun, SVNAdminArea wc) throws SVNException { final SVNPatchTarget target = this; if (target.deleted) { if (!dryRun) { /* * Schedule the target for deletion. Suppress notification, * we'll do it manually in a minute. Also suppress cancellation. */ SVNWCManager.delete(wc.getWCAccess(), wc, target.getAbsPath(), false, false); } } else { /* * If the target's parent directory does not yet exist we need to * create it before we can copy the patched result in place. */ if (target.isAdded() && !target.isParentDirExists()) { /* Check if we can safely create the target's parent. */ File absPath = absWCPath; String[] components = decomposePath(target.getRelPath()); int present_components = 0; for (int i = 0; i < components.length - 1; i++) { final String component = components[i]; absPath = new File(absPath, component); final SVNEntry entry = wc.getWCAccess().getEntry(absPath, false); final SVNNodeKind kind = entry != null ? entry.getKind() : null; if (kind == SVNNodeKind.FILE) { /* Obstructed. */ target.skipped = true; break; } else if (kind == SVNNodeKind.DIR) { /* * ### wc-ng should eventually be able to replace * directories in-place, so this schedule conflict check * will go away. We could then also make the * svn_wc__node_get_kind() call above ignore hidden * nodes. */ if (entry.isDeleted()) { target.skipped = true; break; } present_components++; } else { /* * The WC_DB doesn't know much about this node. Check * what's on disk. */ final SVNFileType disk_kind = SVNFileType.getType(absPath); if (disk_kind != SVNFileType.NONE) { /* An unversioned item is in the way. */ target.skipped = true; break; } } } if (!target.isSkipped()) { absPath = absWCPath; for (int i = 0; i < present_components; i++) { final String component = components[i]; absPath = new File(absPath, component); if (dryRun) { /* Just do notification. */ SVNEvent mergeCompletedEvent = SVNEventFactory.createSVNEvent(absPath, SVNNodeKind.NONE, null, SVNRepository.INVALID_REVISION, SVNStatusType.INAPPLICABLE, SVNStatusType.INAPPLICABLE, SVNStatusType.LOCK_INAPPLICABLE, SVNEventAction.ADD, null, null, null); wc.getWCAccess().handleEvent(mergeCompletedEvent); } else { /* * Create the missing component and add it to * version control. Suppress cancellation. */ if (absPath.mkdirs()) { SVNWCManager.add(absPath, wc, null, SVNRepository.INVALID_REVISION, SVNDepth.INFINITY); } } } } } if (!dryRun && !target.isSkipped()) { /* Copy the patched file on top of the target file. */ SVNFileUtil.copyFile(target.getPatchedPath(), target.getAbsPath(), false); if (target.isAdded()) { /* * The target file didn't exist previously, so add it to * version control. Suppress notification, we'll do that * later. Also suppress cancellation. */ SVNWCManager.add(target.getAbsPath(), wc, null, SVNRepository.INVALID_REVISION, SVNDepth.INFINITY); } /* Restore the target's executable bit if necessary. */ SVNFileUtil.setExecutable(target.getAbsPath(), target.isExecutable()); } } /* Write out rejected hunks, if any. */ if (!dryRun && !target.skipped && target.hadRejects) { final String rej_path = target.getAbsPath().getPath() + ".svnpatch.rej"; SVNFileUtil.copyFile(target.getRejectPath(), new File(rej_path), true); /* ### TODO mark file as conflicted. */ } } public static String[] decomposePath(File path) { String pathString = SVNFileUtil.getFilePath(path); if (pathString.endsWith("/")) { pathString = pathString.substring(0, pathString.length() - 1); } return pathString.split(String.valueOf(File.separatorChar)); } /** * Apply a PATCH to a working copy at ABS_WC_PATH. * * STRIP_COUNT specifies the number of leading path components which should * be stripped from target paths in the patch. * * @throws SVNException * @throws IOException */ public static SVNPatchTarget applyPatch(SVNPatch patch, File absWCPath, int stripCount, SVNAdminArea wc) throws SVNException, IOException { final SVNPatchTarget target = SVNPatchTarget.initPatchTarget(patch, absWCPath, stripCount, wc); if (target.skipped) { return target; } /* Match hunks. */ for (final Iterator i = patch.getHunks().iterator(); i.hasNext();) { final SVNPatchHunk hunk = (SVNPatchHunk) i.next(); SVNPatchHunkInfo hi; int fuzz = 0; /* * Determine the line the hunk should be applied at. If no match is * found initially, try with fuzz. */ do { hi = target.getHunkInfo(hunk, fuzz); fuzz++; } while (hi.isRejected() && fuzz <= MAX_FUZZ); target.hunks.add(hi); } /* Apply or reject hunks. */ for (final Iterator i = target.hunks.iterator(); i.hasNext();) { final SVNPatchHunkInfo hi = (SVNPatchHunkInfo) i.next(); if (hi.isRejected()) { target.rejectHunk(hi); } else { target.applyHunk(hi); } } if (target.kind == SVNNodeKind.FILE) { /* Copy any remaining lines to target. */ target.copyLinesToTarget(0); if (!target.eof) { /* * We could not copy the entire target file to the temporary * file, and would truncate the target if we copied the * temporary file on top of it. Cancel any modifications to the * target file and report is as skipped. */ target.skipped = true; } } /* * Close the streams of the target so that their content is flushed to * disk. This will also close underlying streams. */ if (target.getKind() == SVNNodeKind.FILE) { target.stream.close(); } target.patched.close(); target.reject.close(); if (!target.skipped) { /* * Get sizes of the patched temporary file and the working file. * We'll need those to figure out whether we should add or delete * the patched file. */ final long patchedFileSize = target.patchedPath.length(); final long workingFileSize = target.kind == SVNNodeKind.FILE ? target.absPath.length() : 0; if (patchedFileSize == 0 && workingFileSize > 0) { /* * If a unidiff removes all lines from a file, that usually * means deletion, so we can confidently schedule the target for * deletion. In the rare case where the unidiff was really meant * to replace a file with an empty one, this may not be * desirable. But the deletion can easily be reverted and * creating an empty file manually is not exactly hard either. */ target.deleted = target.kind != SVNNodeKind.NONE; } else if (workingFileSize == 0 && patchedFileSize == 0) { /* * The target was empty or non-existent to begin with and * nothing has changed by patching. Report this as skipped if it * didn't exist. */ if (target.kind != SVNNodeKind.FILE) target.skipped = true; } else if (target.kind != SVNNodeKind.FILE && patchedFileSize > 0) { /* The patch has created a file. */ target.added = true; } } return target; } /** * Determine the line at which a HUNK applies to the TARGET file, and return * an appropriate hunk_info object in *HI, allocated from RESULT_POOL. Use * fuzz factor FUZZ. Set HI->FUZZ to FUZZ. If no correct line can be * determined, set HI->REJECTED to TRUE. When this function returns, neither * TARGET->CURRENT_LINE nor the file offset in the target file will have * changed. * * @throws SVNException * @throws IOException */ public SVNPatchHunkInfo getHunkInfo(final SVNPatchHunk hunk, final int fuzz) throws SVNException, IOException { final SVNPatchTarget target = this; int matchedLine; /* * An original offset of zero means that this hunk wants to create a new * file. Don't bother matching hunks in that case, since the hunk * applies at line 1. If the file already exists, the hunk is rejected. */ if (hunk.getOriginal().getStart() == 0) { if (target.getKind() == SVNNodeKind.FILE) { matchedLine = 0; } else { matchedLine = 1; } } else if (hunk.getOriginal().getStart() > 0 && target.getKind() == SVNNodeKind.FILE) { int savedLine = target.getCurrentLine(); boolean savedEof = target.isEof(); /* * Scan for a match at the line where the hunk thinks it should be * going. */ target.seekToLine(hunk.getOriginal().getStart()); matchedLine = target.scanForMatch(hunk, true, hunk.getOriginal().getStart() + 1, fuzz); if (matchedLine != hunk.getOriginal().getStart()) { /* Scan the whole file again from the start. */ target.seekToLine(1); /* * Scan forward towards the hunk's line and look for a line * where the hunk matches. */ matchedLine = target.scanForMatch(hunk, false, hunk.getOriginal().getStart(), fuzz); /* * In tie-break situations, we arbitrarily prefer early matches * to save us from scanning the rest of the file. */ if (matchedLine == 0) { /* * Scan forward towards the end of the file and look for a * line where the hunk matches. */ matchedLine = target.scanForMatch(hunk, true, 0, fuzz); } } target.seekToLine(savedLine); target.eof = savedEof; } else { /* The hunk wants to modify a file which doesn't exist. */ matchedLine = 0; } return new SVNPatchHunkInfo(hunk, matchedLine, (matchedLine == 0), fuzz); } /** * Scan lines of TARGET for a match of the original text of HUNK, up to but * not including the specified UPPER_LINE. Use fuzz factor FUZZ. If * UPPER_LINE is zero scan until EOF occurs when reading from TARGET. Return * the line at which HUNK was matched in *MATCHED_LINE. If the hunk did not * match at all, set *MATCHED_LINE to zero. If the hunk matched multiple * times, and MATCH_FIRST is TRUE, return the line number at which the first * match occured in *MATCHED_LINE. If the hunk matched multiple times, and * MATCH_FIRST is FALSE, return the line number at which the last match * occured in *MATCHED_LINE. * * @throws SVNException * @throws IOException */ public int scanForMatch(SVNPatchHunk hunk, boolean matchFirst, int upperLine, int fuzz) throws SVNException, IOException { final SVNPatchTarget target = this; int matched_line = 0; while ((target.currentLine < upperLine || upperLine == 0) && !target.eof) { boolean matched = target.matchHunk(hunk, fuzz); if (matched) { boolean taken = false; /* Don't allow hunks to match at overlapping locations. */ for (Iterator i = target.hunks.iterator(); i.hasNext();) { final SVNPatchHunkInfo hi = (SVNPatchHunkInfo) i.next(); taken = (!hi.isRejected() && target.currentLine >= hi.getMatchedLine() && target.currentLine < hi.getMatchedLine() + hi.getHunk().getOriginal().getLength()); if (taken) { break; } } if (!taken) { matched_line = target.currentLine; if (matchFirst) { break; } } } target.seekToLine(target.currentLine + 1); } return matched_line; } /** * Indicate in *MATCHED whether the original text of HUNK matches the patch * TARGET at its current line. Lines within FUZZ lines of the start or end * of HUNK will always match. When this function returns, neither * TARGET->CURRENT_LINE nor the file offset in the target file will have * changed. HUNK->ORIGINAL_TEXT will be reset. * * @throws SVNException * @throws IOException */ private boolean matchHunk(SVNPatchHunk hunk, int fuzz) throws SVNException, IOException { final SVNPatchTarget target = this; final StringBuffer hunkLine = new StringBuffer(); final StringBuffer targetLine = new StringBuffer(); final StringBuffer eol_str = new StringBuffer(); int linesRead; int savedLine; boolean hunkEof; boolean linesMatched; boolean matched = false; if (target.eof) { return matched; } savedLine = target.currentLine; linesRead = 0; linesMatched = false; hunk.getOriginalText().reset(); do { String hunk_line_translated; hunkLine.setLength(0); eol_str.setLength(0); hunkEof = hunk.getOriginalText().readLineWithEol(hunkLine, eol_str); /* Contract keywords, if any, before matching. */ final byte[] eol = eol_str.toString().getBytes(); hunk_line_translated = SVNTranslator.translateString(hunkLine.toString(), eol, target.keywords, false, false); linesRead++; targetLine.setLength(0); target.readLine(targetLine); if (!hunkEof) { if (linesRead <= fuzz && hunk.getLeadingContext() > fuzz) { linesMatched = true; } else if (linesRead > hunk.getOriginal().getLength() - fuzz && hunk.getTrailingContext() > fuzz) { linesMatched = true; } else { linesMatched = hunk_line_translated.equals(targetLine.toString()); } } } while (linesMatched && !(hunkEof || target.eof)); if (hunkEof) { matched = linesMatched; } else if (target.eof) { /* * If the target has no newline at end-of-file, we get an EOF * indication for the target earlier than we do get it for the hunk. */ hunkEof = hunk.getOriginalText().readLineWithEol(hunkLine, null); if (hunkLine.length() == 0 && hunkEof) { matched = linesMatched; } else { matched = false; } } target.seekToLine(savedLine); target.eof = false; return matched; } /** * Attempt to write LEN bytes of DATA to STREAM, the underlying file of * which is at ABSPATH. Fail if not all bytes could be written to the * stream. */ private void tryWrite(OutputStream stream, StringBuffer buffer) throws IOException { stream.write(buffer.toString().getBytes()); } /** * Use client context CTX to send a suitable notification for a patch * TARGET. * * @throws SVNException */ public void sendPatchNotification(SVNAdminArea wc) throws SVNException { final SVNPatchTarget target = this; final ISVNEventHandler eventHandler = wc.getWCAccess().getEventHandler(); if (eventHandler == null) { return; } SVNEventAction action; if (target.skipped) { action = SVNEventAction.SKIP; } else if (target.deleted) { action = SVNEventAction.DELETE; } else if (target.added) { action = SVNEventAction.ADD; } else { action = SVNEventAction.PATCH; } SVNStatusType contentState = SVNStatusType.INAPPLICABLE; if (action == SVNEventAction.SKIP) { if (target.parentDirExists && (target.kind == SVNNodeKind.NONE || target.kind == SVNNodeKind.UNKNOWN)) { contentState = SVNStatusType.MISSING; } else if (target.kind == SVNNodeKind.DIR) { contentState = SVNStatusType.OBSTRUCTED; } else { contentState = SVNStatusType.UNKNOWN; } } else { if (target.hadRejects) { contentState = SVNStatusType.CONFLICTED; } else if (target.localMods) { contentState = SVNStatusType.MERGED; } else { contentState = SVNStatusType.CHANGED; } } final SVNEvent notify = SVNEventFactory.createSVNEvent(target.absPath != null ? target.absPath : target.relPath, target.kind, null, 0, contentState, SVNStatusType.INAPPLICABLE, SVNStatusType.LOCK_INAPPLICABLE, action, null, null, null); eventHandler.handleEvent(notify, ISVNEventHandler.UNKNOWN); if (action == SVNEventAction.PATCH) { for (final Iterator i = target.hunks.iterator(); i.hasNext();) { final SVNPatchHunkInfo hi = (SVNPatchHunkInfo) i.next(); if (hi.isRejected()) { action = SVNEventAction.PATCH_REJECTED_HUNK; } else { action = SVNEventAction.PATCH_APPLIED_HUNK; } final SVNEvent notify2 = SVNEventFactory.createSVNEvent(target.absPath != null ? target.absPath : target.relPath, target.kind, null, 0, action, null, null, null); notify2.setInfo(hi); eventHandler.handleEvent(notify2, ISVNEventHandler.UNKNOWN); } } } }