com.cloudbees.diff.ContextualPatch.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudbees.diff.ContextualPatch.java

Source

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */
package com.cloudbees.diff;

import org.apache.commons.io.IOUtils;

import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Applies contextual patches to files. The patch file can contain patches for multiple files.
 *
 * @author Maros Sandor
 */
public final class ContextualPatch {

    public static final String MAGIC = "# This patch file was generated by NetBeans IDE"; // NOI18N    

    // first seen in mercurial diffs: characters after the second @@ - ignore them  
    private final Pattern unifiedRangePattern = Pattern.compile("@@ -(\\d+)(,\\d+)? \\+(\\d+)(,\\d+)? @@(\\s.*)?");
    private final Pattern baseRangePattern = Pattern.compile("\\*\\*\\* (\\d+)(,\\d+)? \\*\\*\\*\\*");
    private final Pattern modifiedRangePattern = Pattern.compile("--- (\\d+)(,\\d+)? ----");
    private final Pattern normalChangeRangePattern = Pattern.compile("(\\d+),(\\d+)c(\\d+),(\\d+)");
    private final Pattern normalAddRangePattern = Pattern.compile("(\\d+)a(\\d+),(\\d+)");
    private final Pattern normalDeleteRangePattern = Pattern.compile("(\\d+),(\\d+)d(\\d+)");
    private final Pattern binaryHeaderPattern = Pattern.compile("MIME: (.*?); encoding: (.*?); length: (-?\\d+?)");

    private final File patchFile;
    private final File suggestedContext;

    private File context;
    private BufferedReader patchReader;
    private String patchLine;
    private boolean patchLineRead;
    private int lastPatchedLine; // the last line that was successfuly patched

    private ContextualPatch(File patchFile, File context) {
        this.patchFile = patchFile;
        this.suggestedContext = context;
    }

    public static ContextualPatch create(File patchFile, File context) {
        return new ContextualPatch(patchFile, context);
    }

    /**
     * @param dryRun true if the method should not make any modifications to files, false otherwise
     * @return
     * @throws PatchException
     * @throws IOException
     */
    public List<PatchReport> patch(boolean dryRun) throws PatchException, IOException {
        List<PatchReport> report = new ArrayList<PatchReport>();
        init();
        try {
            patchLine = patchReader.readLine();
            List<SinglePatch> patches = new ArrayList<SinglePatch>();
            for (;;) {
                SinglePatch patch = getNextPatch();
                if (patch == null)
                    break;
                patches.add(patch);
            }
            computeContext(patches);
            for (SinglePatch patch : patches) {
                try {
                    applyPatch(patch, dryRun);
                    report.add(new PatchReport(patch.targetFile, computeBackup(patch.targetFile), patch.binary,
                            PatchStatus.Patched, null));
                } catch (Exception e) {
                    report.add(new PatchReport(patch.targetFile, null, patch.binary, PatchStatus.Failure, e));
                }
            }
            return report;
        } finally {
            if (patchReader != null)
                try {
                    patchReader.close();
                } catch (IOException e) {
                }
        }
    }

    private void init() throws IOException {
        patchReader = new BufferedReader(new FileReader(patchFile));
        String encoding = "ISO-8859-1";
        String line = patchReader.readLine();
        if (MAGIC.equals(line)) {
            encoding = "utf8"; // NOI18N
            line = patchReader.readLine();
        }
        patchReader.close();

        byte[] buffer = new byte[MAGIC.length()];
        InputStream in = new FileInputStream(patchFile);
        int read = in.read(buffer);
        in.close();
        if (read != -1 && MAGIC.equals(new String(buffer, "utf8"))) { // NOI18N
            encoding = "utf8"; // NOI18N
        }
        patchReader = new BufferedReader(new InputStreamReader(new FileInputStream(patchFile), encoding));
    }

    private void applyPatch(SinglePatch patch, boolean dryRun) throws IOException, PatchException {
        lastPatchedLine = 1;
        List<String> target;
        patch.targetFile = computeTargetFile(patch);
        if (patch.targetFile.exists() && !patch.binary) {
            target = readFile(patch.targetFile);
            if (patchCreatesNewFileThatAlreadyExists(patch, target))
                return;
        } else {
            target = new ArrayList<String>();
        }
        if (patch.mode == Mode.DELETE) {
            target = new ArrayList<String>();
        } else {
            if (!patch.binary) {
                for (Hunk hunk : patch.hunks) {
                    applyHunk(target, hunk);
                }
            }
        }
        if (!dryRun) {
            //backup(patch.targetFile);
            writeFile(patch, target);
        }
    }

    private boolean patchCreatesNewFileThatAlreadyExists(SinglePatch patch, List<String> originalFile)
            throws PatchException {
        if (patch.hunks.length != 1)
            return false;
        Hunk hunk = patch.hunks[0];
        if (hunk.baseStart != 0 || hunk.baseCount != 0 || hunk.modifiedStart != 1
                || hunk.modifiedCount != originalFile.size())
            return false;

        List<String> target = new ArrayList<String>(hunk.modifiedCount);
        applyHunk(target, hunk);
        return target.equals(originalFile);
    }

    private void backup(File target) throws IOException {
        if (target.exists()) {
            copyStreamsCloseAll(new FileOutputStream(computeBackup(target)), new FileInputStream(target));
        }
    }

    private File computeBackup(File target) {
        return new File(target.getParentFile(), target.getName() + ".original~");
    }

    private void copyStreamsCloseAll(OutputStream writer, InputStream reader) throws IOException {
        byte[] buffer = new byte[4096];
        int n;
        while ((n = reader.read(buffer)) != -1) {
            writer.write(buffer, 0, n);
        }
        writer.close();
        reader.close();
    }

    private void writeFile(SinglePatch patch, List<String> lines) throws IOException {
        if (patch.mode == Mode.DELETE) {
            patch.targetFile.delete();
            return;
        }

        patch.targetFile.getParentFile().mkdirs();
        if (patch.binary) {
            if (patch.hunks.length == 0) {
                patch.targetFile.delete();
            } else {
                byte[] content = Base64.decode(patch.hunks[0].lines);
                copyStreamsCloseAll(new FileOutputStream(patch.targetFile), new ByteArrayInputStream(content));
            }
        } else {
            PrintWriter w = new PrintWriter(
                    new OutputStreamWriter(new FileOutputStream(patch.targetFile), getEncoding(patch.targetFile)));
            try {
                if (lines.size() == 0)
                    return;
                for (String line : lines.subList(0, lines.size() - 1)) {
                    w.println(line);
                }
                w.print(lines.get(lines.size() - 1));
                if (!patch.noEndingNewline) {
                    w.println();
                }
            } finally {
                w.close();
            }
        }
    }

    private void applyHunk(List<String> target, Hunk hunk) throws PatchException {
        int idx = findHunkIndex(target, hunk);
        if (idx == -1)
            throw new PatchException("Cannot apply hunk @@ " + hunk.baseCount);
        applyHunk(target, hunk, idx, false);
    }

    private int findHunkIndex(List<String> target, Hunk hunk) throws PatchException {
        int idx = hunk.modifiedStart; // first guess from the hunk range specification
        if (idx >= lastPatchedLine && applyHunk(target, hunk, idx, true)) {
            return idx;
        } else {
            // try to search for the context
            for (int i = idx - 1; i >= lastPatchedLine; i--) {
                if (applyHunk(target, hunk, i, true)) {
                    return i;
                }
            }
            for (int i = idx + 1; i < target.size(); i++) {
                if (applyHunk(target, hunk, i, true)) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * @return true if the application succeeded
     */
    private boolean applyHunk(List<String> target, Hunk hunk, int idx, boolean dryRun) throws PatchException {
        idx--; // indices in the target list are 0-based
        for (String hunkLine : hunk.lines) {
            boolean isAddition = isAdditionLine(hunkLine);
            if (!isAddition) {
                String targetLine = target.get(idx).trim();
                if (!targetLine.equals(hunkLine.substring(1).trim())) { // be optimistic, compare trimmed context lines
                    if (dryRun) {
                        return false;
                    } else {
                        throw new PatchException("Unapplicable hunk @@ " + hunk.baseStart);
                    }
                }
            }
            if (dryRun) {
                if (isAddition) {
                    idx--;
                }
            } else {
                if (isAddition) {
                    target.add(idx, hunkLine.substring(1));
                } else if (isRemovalLine(hunkLine)) {
                    target.remove(idx);
                    idx--;
                }
            }
            idx++;
        }
        idx++; // indices in the target list are 0-based
        lastPatchedLine = idx;
        return true;
    }

    private boolean isAdditionLine(String hunkLine) {
        return hunkLine.charAt(0) == '+';
    }

    private boolean isRemovalLine(String hunkLine) {
        return hunkLine.charAt(0) == '-';
    }

    private Charset getEncoding(File file) {
        return Charset.defaultCharset();
    }

    private List<String> readFile(File target) throws IOException {
        BufferedReader r = new BufferedReader(
                new InputStreamReader(new FileInputStream(target), getEncoding(target)));
        try {
            List<String> lines = new ArrayList<String>();
            String line;
            while ((line = r.readLine()) != null) {
                lines.add(line);
            }
            return lines;
        } finally {
            IOUtils.closeQuietly(r);
        }
    }

    private SinglePatch getNextPatch() throws IOException, PatchException {
        SinglePatch patch = new SinglePatch();
        for (;;) {
            String line = readPatchLine();
            if (line == null)
                return null;

            if (line.startsWith("Index:")) {
                patch.targetPath = line.substring(6).trim();
            } else if (line.startsWith("MIME: application/octet-stream;")) {
                unreadPatchLine();
                readBinaryPatchContent(patch);
                break;
            } else if (line.startsWith("--- ")) {
                unreadPatchLine();
                readPatchContent(patch);
                break;
            } else if (line.startsWith("*** ")) {
                unreadPatchLine();
                readContextPatchContent(patch);
                break;
            } else if (isNormalDiffRange(line)) {
                unreadPatchLine();
                readNormalPatchContent(patch);
                break;
            }
        }
        return patch;
    }

    private boolean isNormalDiffRange(String line) {
        return normalAddRangePattern.matcher(line).matches() || normalChangeRangePattern.matcher(line).matches()
                || normalDeleteRangePattern.matcher(line).matches();
    }

    /**
     * Reads binary diff hunk.
     */
    private void readBinaryPatchContent(SinglePatch patch) throws PatchException, IOException {
        List<Hunk> hunks = new ArrayList<Hunk>();
        Hunk hunk = new Hunk();
        for (;;) {
            String line = readPatchLine();
            if (line == null || line.startsWith("Index:") || line.length() == 0) {
                unreadPatchLine();
                break;
            }
            if (patch.binary) {
                hunk.lines.add(line);
            } else {
                Matcher m = binaryHeaderPattern.matcher(line);
                if (m.matches()) {
                    patch.binary = true;
                    int length = Integer.parseInt(m.group(3));
                    if (length == -1)
                        break;
                    hunks.add(hunk);
                }
            }
        }
        patch.hunks = hunks.toArray(new Hunk[hunks.size()]);
    }

    /**
     * Reads normal diff hunks.
     */
    private void readNormalPatchContent(SinglePatch patch) throws IOException, PatchException {
        List<Hunk> hunks = new ArrayList<Hunk>();
        Hunk hunk = null;
        Matcher m;
        for (;;) {
            String line = readPatchLine();
            if (line == null || line.startsWith("Index:")) {
                unreadPatchLine();
                break;
            }
            if ((m = normalAddRangePattern.matcher(line)).matches()) {
                hunk = new Hunk();
                hunks.add(hunk);
                parseNormalRange(hunk, m);
            } else if ((m = normalChangeRangePattern.matcher(line)).matches()) {
                hunk = new Hunk();
                hunks.add(hunk);
                parseNormalRange(hunk, m);
            } else if ((m = normalDeleteRangePattern.matcher(line)).matches()) {
                hunk = new Hunk();
                hunks.add(hunk);
                parseNormalRange(hunk, m);
            } else {
                if (line.startsWith("> ")) {
                    hunk.lines.add("+" + line.substring(2));
                } else if (line.startsWith("< ")) {
                    hunk.lines.add("-" + line.substring(2));
                } else if (line.startsWith("---")) {
                    // ignore
                } else {
                    throw new PatchException("Invalid hunk line: " + line);
                }
            }
        }
        patch.hunks = hunks.toArray(new Hunk[hunks.size()]);
    }

    private void parseNormalRange(Hunk hunk, Matcher m) {
        if (m.pattern() == normalAddRangePattern) {
            hunk.baseStart = Integer.parseInt(m.group(1));
            hunk.baseCount = 0;
            hunk.modifiedStart = Integer.parseInt(m.group(2));
            hunk.modifiedCount = Integer.parseInt(m.group(3)) - hunk.modifiedStart + 1;
        } else if (m.pattern() == normalDeleteRangePattern) {
            hunk.baseStart = Integer.parseInt(m.group(1));
            hunk.baseCount = Integer.parseInt(m.group(2)) - hunk.baseStart + 1;
            hunk.modifiedStart = Integer.parseInt(m.group(3));
            hunk.modifiedCount = 0;
        } else {
            hunk.baseStart = Integer.parseInt(m.group(1));
            hunk.baseCount = Integer.parseInt(m.group(2)) - hunk.baseStart + 1;
            hunk.modifiedStart = Integer.parseInt(m.group(3));
            hunk.modifiedCount = Integer.parseInt(m.group(4)) - hunk.modifiedStart + 1;
        }
    }

    /**
     * Reads context diff hunks.
     */
    private void readContextPatchContent(SinglePatch patch) throws IOException, PatchException {
        String base = readPatchLine();
        if (base == null || !base.startsWith("*** "))
            throw new PatchException("Invalid context diff header: " + base);
        String modified = readPatchLine();
        if (modified == null || !modified.startsWith("--- "))
            throw new PatchException("Invalid context diff header: " + modified);
        if (patch.targetPath == null) {
            computeTargetPath(base, modified, patch);
        }

        List<Hunk> hunks = new ArrayList<Hunk>();
        Hunk hunk = null;

        int lineCount = -1;
        for (;;) {
            String line = readPatchLine();
            if (line == null || line.length() == 0 || line.startsWith("Index:")) {
                unreadPatchLine();
                break;
            } else if (line.startsWith("***************")) {
                hunk = new Hunk();
                parseContextRange(hunk, readPatchLine());
                hunks.add(hunk);
            } else if (line.startsWith("--- ")) {
                lineCount = 0;
                parseContextRange(hunk, line);
                hunk.lines.add(line);
            } else {
                char c = line.charAt(0);
                if (c == ' ' || c == '+' || c == '-' || c == '!') {
                    if (lineCount < hunk.modifiedCount) {
                        hunk.lines.add(line);
                        if (lineCount != -1) {
                            lineCount++;
                        }
                    }
                } else {
                    throw new PatchException("Invalid hunk line: " + line);
                }
            }
        }
        patch.hunks = hunks.toArray(new Hunk[hunks.size()]);
        convertContextToUnified(patch);
    }

    private void convertContextToUnified(SinglePatch patch) throws PatchException {
        Hunk[] unifiedHunks = new Hunk[patch.hunks.length];
        int idx = 0;
        for (Hunk hunk : patch.hunks) {
            unifiedHunks[idx++] = convertContextToUnified(hunk);
        }
        patch.hunks = unifiedHunks;
    }

    private Hunk convertContextToUnified(Hunk hunk) throws PatchException {
        Hunk unifiedHunk = new Hunk();
        unifiedHunk.baseStart = hunk.baseStart;
        unifiedHunk.modifiedStart = hunk.modifiedStart;
        int split = -1;
        for (int i = 0; i < hunk.lines.size(); i++) {
            if (hunk.lines.get(i).startsWith("--- ")) {
                split = i;
                break;
            }
        }
        if (split == -1)
            throw new PatchException("Missing split divider in context patch");

        int baseIdx = 0;
        int modifiedIdx = split + 1;
        List<String> unifiedLines = new ArrayList<String>(hunk.lines.size());
        for (; baseIdx < split || modifiedIdx < hunk.lines.size();) {
            String baseLine = baseIdx < split ? hunk.lines.get(baseIdx) : "~";
            String modifiedLine = modifiedIdx < hunk.lines.size() ? hunk.lines.get(modifiedIdx) : "~";
            if (baseLine.startsWith("- ")) {
                unifiedLines.add("-" + baseLine.substring(2));
                unifiedHunk.baseCount++;
                baseIdx++;
            } else if (modifiedLine.startsWith("+ ")) {
                unifiedLines.add("+" + modifiedLine.substring(2));
                unifiedHunk.modifiedCount++;
                modifiedIdx++;
            } else if (baseLine.startsWith("! ")) {
                unifiedLines.add("-" + baseLine.substring(2));
                unifiedHunk.baseCount++;
                baseIdx++;
            } else if (modifiedLine.startsWith("! ")) {
                unifiedLines.add("+" + modifiedLine.substring(2));
                unifiedHunk.modifiedCount++;
                modifiedIdx++;
            } else if (baseLine.startsWith("  ") && modifiedLine.startsWith("  ")) {
                unifiedLines.add(baseLine.substring(1));
                unifiedHunk.baseCount++;
                unifiedHunk.modifiedCount++;
                baseIdx++;
                modifiedIdx++;
            } else if (baseLine.startsWith("  ")) {
                unifiedLines.add(baseLine.substring(1));
                unifiedHunk.baseCount++;
                unifiedHunk.modifiedCount++;
                baseIdx++;
            } else if (modifiedLine.startsWith("  ")) {
                unifiedLines.add(modifiedLine.substring(1));
                unifiedHunk.baseCount++;
                unifiedHunk.modifiedCount++;
                modifiedIdx++;
            } else {
                throw new PatchException("Invalid context patch: " + baseLine);
            }
        }
        unifiedHunk.lines = unifiedLines;
        return unifiedHunk;
    }

    /**
     * Reads unified diff hunks.
     */
    private void readPatchContent(SinglePatch patch) throws IOException, PatchException {
        String base = readPatchLine();
        if (base == null || !base.startsWith("--- "))
            throw new PatchException("Invalid unified diff header: " + base);
        String modified = readPatchLine();
        if (modified == null || !modified.startsWith("+++ "))
            throw new PatchException("Invalid unified diff header: " + modified);
        if (patch.targetPath == null) {
            computeTargetPath(base, modified, patch);
        }

        List<Hunk> hunks = new ArrayList<Hunk>();
        Hunk hunk = null;

        for (;;) {
            String line = readPatchLine();
            if (line == null || line.length() == 0 || line.startsWith("Index:")) {
                unreadPatchLine();
                break;
            }
            char c = line.charAt(0);
            if (c == '@') {
                hunk = new Hunk();
                parseRange(hunk, line);
                hunks.add(hunk);
            } else if (c == ' ' || c == '+' || c == '-') {
                hunk.lines.add(line);
            } else if (line.equals(Hunk.ENDING_NEWLINE)) {
                patch.noEndingNewline = true;
            } else {
                // first seen in mercurial diffs: be optimistic, this is probably the end of this patch  
                unreadPatchLine();
                break;
            }
        }
        patch.hunks = hunks.toArray(new Hunk[hunks.size()]);
    }

    private void computeTargetPath(String base, String modified, SinglePatch patch) {
        base = base.substring("+++ ".length());
        modified = modified.substring("--- ".length());
        // first seen in mercurial diffs: base and modified paths are different: base starts with "a/" and modified starts with "b/"
        if ((base.startsWith("/dev/null") || base.startsWith("a/"))
                && (modified.equals("/dev/null") || modified.startsWith("b/"))) {
            if (base.startsWith("a/"))
                base = base.substring(2);
            if (modified.startsWith("b/"))
                modified = modified.substring(2);
        }
        base = untilTab(base).trim();
        if (base.equals("/dev/null")) {
            // "/dev/null" in base indicates a new file
            patch.targetPath = untilTab(modified).trim();
            patch.mode = Mode.ADD;
        } else {
            patch.targetPath = base;
            patch.mode = modified.startsWith("/dev/null") ? Mode.DELETE : Mode.CHANGE;
        }
    }

    private String untilTab(String base) {
        int pathEndIdx = base.indexOf('\t');
        if (pathEndIdx > 0)
            base = base.substring(0, pathEndIdx);
        return base;
    }

    private void parseRange(Hunk hunk, String range) throws PatchException {
        Matcher m = unifiedRangePattern.matcher(range);
        if (!m.matches())
            throw new PatchException("Invalid unified diff range: " + range);
        hunk.baseStart = Integer.parseInt(m.group(1));
        hunk.baseCount = m.group(2) != null ? Integer.parseInt(m.group(2).substring(1)) : 1;
        hunk.modifiedStart = Integer.parseInt(m.group(3));
        hunk.modifiedCount = m.group(4) != null ? Integer.parseInt(m.group(4).substring(1)) : 1;
    }

    private void parseContextRange(Hunk hunk, String range) throws PatchException {
        if (range.charAt(0) == '*') {
            Matcher m = baseRangePattern.matcher(range);
            if (!m.matches())
                throw new PatchException("Invalid context diff range: " + range);
            hunk.baseStart = Integer.parseInt(m.group(1));
            hunk.baseCount = m.group(2) != null ? Integer.parseInt(m.group(2).substring(1)) : 1;
            hunk.baseCount -= hunk.baseStart - 1;
        } else {
            Matcher m = modifiedRangePattern.matcher(range);
            if (!m.matches())
                throw new PatchException("Invalid context diff range: " + range);
            hunk.modifiedStart = Integer.parseInt(m.group(1));
            hunk.modifiedCount = m.group(2) != null ? Integer.parseInt(m.group(2).substring(1)) : 1;
            hunk.modifiedCount -= hunk.modifiedStart - 1;
        }
    }

    private String readPatchLine() throws IOException {
        if (patchLineRead) {
            patchLine = patchReader.readLine();
        } else {
            patchLineRead = true;
        }
        return patchLine;
    }

    private void unreadPatchLine() {
        patchLineRead = false;
    }

    private void computeContext(List<SinglePatch> patches) {
        File bestContext = suggestedContext;
        int bestContextMatched = 0;
        for (context = suggestedContext; context != null; context = context.getParentFile()) {
            int patchedFiles = 0;
            for (SinglePatch patch : patches) {
                try {
                    applyPatch(patch, true);
                    patchedFiles++;
                } catch (Exception e) {
                    // patch failed to apply
                }
            }
            if (patchedFiles > bestContextMatched) {
                bestContextMatched = patchedFiles;
                bestContext = context;
                if (patchedFiles == patches.size())
                    break;
            }
        }
        context = bestContext;
    }

    private File computeTargetFile(SinglePatch patch) {
        if (patch.targetPath == null) {
            patch.targetPath = context.getAbsolutePath();
        }
        if (context.isFile())
            return context;
        return new File(context, patch.targetPath);
    }

    enum Mode {
        /**
         * Update to existing file
         */
        CHANGE,
        /**
         * Adding a new file
         */
        ADD,
        /**
         * Deleting an existing file
         */
        DELETE
    }

    public static enum PatchStatus {
        Patched, Missing, Failure
    }

    private static class SinglePatch {
        String targetIndex;
        String targetPath;
        Hunk[] hunks;
        boolean targetMustExist = true; // == false if the patch contains one hunk with just additions ('+' lines)
        File targetFile; // computed later
        boolean noEndingNewline; // resulting file should not end with a newline
        boolean binary; // binary patches contain one encoded Hunk
        Mode mode;
    }

    ;

    public static final class PatchReport {

        private File file;
        private File originalBackupFile;
        private boolean binary;
        private PatchStatus status;
        private Throwable failure;

        PatchReport(File file, File originalBackupFile, boolean binary, PatchStatus status, Throwable failure) {
            this.file = file;
            this.originalBackupFile = originalBackupFile;
            this.binary = binary;
            this.status = status;
            this.failure = failure;
        }

        public File getFile() {
            return file;
        }

        public File getOriginalBackupFile() {
            return originalBackupFile;
        }

        public boolean isBinary() {
            return binary;
        }

        public PatchStatus getStatus() {
            return status;
        }

        public Throwable getFailure() {
            return failure;
        }
    }
}