/* * UnifiedEmitter.java * * Copyright (C) 2009-12 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.studio.client.workbench.views.vcs.common.diff; import org.rstudio.core.client.DuplicateHelper; import org.rstudio.studio.client.workbench.views.vcs.common.diff.Line.Type; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; /** * This class is used to subset an existing patch (the existing patch is * modeled in DiffChunk and the subset of changes we want to keep is zero * or more ArrayList<Line>). * * It works by using the existing patch to recreate the original data, then * merging with the specific changes we want to keep. * * The output is in "Unified diff" format. * * You can also use this class to generate reverse selective patches (basically * the same as above, but with the effect of undoing the patch on the changed * file, rather than applying the patch to the original file) by simply * reversing the DiffChunk (DiffChunk.reverse()) and lines (Line.reverseLines()) * before calling addDiffs(). */ public class UnifiedEmitter { public UnifiedEmitter(String relPath) { this("a/" + relPath , "b/" + relPath); } public UnifiedEmitter(String fileA, String fileB) { fileA_ = fileA; fileB_ = fileB; } public void addContext(DiffChunk chunk) { contextLines_.addAll(chunk.getLines()); } public void addDiffs(ArrayList<Line> lines) { diffLines_.addAll(lines); } public String createPatch(boolean includeFileHeader) { prepareList(contextLines_, Type.Insertion); prepareList(diffLines_, Type.Same); final ArrayList<DiffChunk> chunks = toDiffChunks( new OutputLinesGenerator(contextLines_, diffLines_).getOutput()); if (chunks.size() == 0) return ""; StringBuilder p = new StringBuilder(); // Write file header if (includeFileHeader) { p.append("--- ").append(fileA_).append(EOL); p.append("+++ ").append(fileB_).append(EOL); } for (DiffChunk chunk : chunks) { p.append(createChunkString(chunk)); p.append(EOL); for (Line line : chunk.getLines()) { switch (line.getType()) { case Same: p.append(' '); break; case Insertion: p.append('+'); break; case Deletion: p.append('-'); break; case Comment: p.append('\\'); break; default: throw new IllegalArgumentException(); } p.append(line.getText()).append(EOL); } } return p.toString(); } public static String createChunkString(DiffChunk chunk) { StringBuilder sb = new StringBuilder(); // Write chunk header: @@ -A,B +C,D @@ Range[] ranges = chunk.getRanges(); for (int i = 0; i < ranges.length; i++) sb.append('@'); for (int i = 0; i < ranges.length - 1; i++) { sb.append(" -").append(ranges[i].startRow); sb.append(',').append(ranges[i].rowCount); } sb.append(" +").append(ranges[ranges.length-1].startRow); sb.append(",").append(ranges[ranges.length-1].rowCount); sb.append(' '); for (int i = 0; i < ranges.length; i++) sb.append('@'); sb.append(chunk.getLineText()); return sb.toString(); } /** * Divide a list of sorted lines into DiffChunks. * * NOTE: If we cared about compact diffs we could detect long runs of * unchanged lines and elide them, like diff tools usually do. (Currently * we keep all the lines we're given, and only use discontinuities to * break up into chunks.) */ private ArrayList<DiffChunk> toDiffChunks(ArrayList<Line> lines) { ArrayList<DiffChunk> chunks = new ArrayList<DiffChunk>(); if (lines.size() == 0) return chunks; int line = lines.get(0).getOldLine(); // The index of the earliest line that hasn't been put into a chunk yet int head = 0; for (int i = 1; i < lines.size(); i++) { if ((lines.get(i).getOldLine() - line) > 1) { // There's a gap between this line and the previous line. Turn // the previous contiguous run into a DiffChunk. List<Line> sublist = lines.subList(head, i); chunks.add(contiguousLinesToChunk(sublist)); // This line is now the start of a new contiguous run. head = i; } line = lines.get(i).getOldLine(); } // Add final contiguous run List<Line> sublist = lines.subList(head, lines.size()); chunks.add(contiguousLinesToChunk(sublist)); return chunks; } private DiffChunk contiguousLinesToChunk(List<Line> sublist) { Line first = sublist.get(0); Line last = sublist.get(sublist.size() - 1); int[] firstLines = first.getLines(); int[] lastLines = last.getLines(); // for purposes of chunk generation we need to not have any rows indicated // as zero for (int i = 0; i < firstLines.length; i++) firstLines[i] = Math.max(1, firstLines[i]); for (int i = 0; i < lastLines.length; i++) lastLines[i] = Math.max(1, lastLines[i]); Range[] ranges = new Range[firstLines.length]; for (int i = 0; i < firstLines.length; i++) ranges[i] = new Range(firstLines[i], 1 + lastLines[i] - firstLines[i]); return new DiffChunk(ranges, "", new ArrayList<Line>(sublist), -1); } private static void prepareList(ArrayList<Line> lines, Type typeToRemove) { // Remove any entries that match the given type for (int i = 0; i < lines.size(); i++) if (lines.get(i).getType() == typeToRemove) lines.remove(i--); // Sort and deduplicate Collections.sort(lines); DuplicateHelper.dedupeSortedList(lines); } /** * Here is where the heavy lifting of merging is done. The only reason this * is factored into a class is to make up for the lack of real closures in * Java. */ private static class OutputLinesGenerator { private final ArrayList<Line> output = new ArrayList<Line>(); private final Iterator<Line> ctxit; // Iterator for all context lines private final Iterator<Line> dffit; // Iterator for all diff lines private Line ctx; // Points to the current context line private Line dff; // Points to the current diff line // Tracks the amount that the "new" line numbers are offset from the "old" // line numbers. new = old + skew private int skew = 0; private OutputLinesGenerator(ArrayList<Line> contextLines, ArrayList<Line> diffLines) { ctxit = contextLines.iterator(); dffit = diffLines.iterator(); // Set ctx and dff to first lines (or null if empty) ctxPop(false); dffPop(false); /** * Now we have two ordered iterators, one for the context (original * document) and one for the diffs we want to apply to it. We want to * merge them together into the output ArrayList in the proper order, * being careful to throw out any context lines that are made obsolete * by the diff lines. */ // Do this while loop while both iterators still have elements while (ctx != null && dff != null) { // Now we have a context line (ctx) and a diff line (dff) in hand. /* Debug.devlogf("DiffIndex: {0}/{1}, {2}-{3}, {4}-{5}", ctx.getDiffIndex(), dff.getDiffIndex(), ctx.getOldLine(), ctx.getNewLine(), dff.getOldLine(), dff.getNewLine()); */ int cmp = ctx.getDiffIndex() - dff.getDiffIndex(); if (cmp < 0) ctxPop(true); else if (cmp > 0) dffPop(true); else { /** * ctx and dff are identical. And since we dropped all Insertions * from contextLines_ and all Sames from diffLines_, we know they * must be either Deletions or Comments. */ if (ctx.getType() == Type.Deletion) { dffPop(true); ctxPop(false); } else if (ctx.getType() == Type.Comment) { dffPop(false); ctxPop(true); } else { throw new IllegalStateException( "Unexpected line type: " + ctx.getType().name()); } } } // Finish off the context iterator if necessary while (ctx != null) ctxPop(true); // Finish off the diff iterator if necessary while (dff != null) dffPop(true); } /** * (Optionally) adds the value of ctx to the output, and then (always) * sets ctx to the next context line */ private void ctxPop(boolean addToOutput) { if (addToOutput) writeContextLine(output, ctx, skew); ctx = ctxit.hasNext() ? ctxit.next() : null; } /** * (Optionally) adds the value of dff to the output, and then (always) * sets dff to the next diff line */ private void dffPop(boolean addToOutput) { if (addToOutput) skew = writeDiffLine(output, dff, skew); dff = dffit.hasNext() ? dffit.next() : null; } private void writeContextLine(ArrayList<Line> output, Line ctx, int skew) { switch (ctx.getType()) { case Same: case Comment: output.add(new Line(ctx.getType(), ctx.getOldLine(), ctx.getOldLine() + skew, ctx.getText(), ctx.getDiffIndex())); break; case Deletion: // This is a line that, in the source diff, was deleted from orig. // But since we're processing it as context, we ignore the delete, // so we turn it back into "Same". output.add(new Line(Type.Same, ctx.getOldLine(), ctx.getOldLine() + skew, ctx.getText(), ctx.getDiffIndex())); break; default: assert false : "Unexpected context line type"; throw new IllegalStateException(); } } private int writeDiffLine(ArrayList<Line> output, Line dff, int skew) { switch (dff.getType()) { case Deletion: output.add(new Line(Type.Deletion, dff.getOldLine(), dff.getOldLine() + skew, dff.getText(), dff.getDiffIndex())); skew--; break; case Insertion: output.add(new Line(Type.Insertion, dff.getOldLine(), dff.getOldLine() + skew, dff.getText(), dff.getDiffIndex())); skew++; break; default: assert false : "Unexpected diff line type"; throw new IllegalStateException(); } return skew; } /** * Get the result */ public ArrayList<Line> getOutput() { return output; } } private final ArrayList<Line> contextLines_ = new ArrayList<Line>(); private final ArrayList<Line> diffLines_ = new ArrayList<Line>(); private final String fileA_; private final String fileB_; private static final String EOL = "\n"; }