/** * Copyright (c) 2009--2012 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package com.redhat.rhn.common.filediff; import java.util.ArrayList; import java.util.List; /** * A single diff trace through two files. * Represents one trace of a linked list of traces. * @version $Rev$ */ public class Trace { private Trace next; private Edit edit; private int currentLineOld; private int currentLineNew; private int matches; /** * @param oldSize The size of the old file. * @param newSize The size of the new file. */ public Trace(int oldSize, int newSize) { //we are going to go backwards through the files for diffing. currentLineOld = oldSize - 1; currentLineNew = newSize - 1; matches = 0; edit = null; next = null; } /** * Private constructor used when forking a trace. * @param currentLineOldIn The current line for the old file. * @param currentLineNewIn The current line for the new file. * @param parentIn The edit, which will be shortly a parent for a delete edit. * @param matchesIn The number of matches in this trace. * @param nextIn The next trace in the linked list. */ private Trace(int currentLineOldIn, int currentLineNewIn, Edit parentIn, int matchesIn, Trace nextIn) { currentLineOld = currentLineOldIn; currentLineNew = currentLineNewIn; edit = parentIn; matches = matchesIn; next = nextIn; this.makeDelete(); } /** * @return The next trace in the linked list. */ public Trace next() { return next; } /** * @param nextIn The next trace in the linked list. */ public void setNext(Trace nextIn) { next = nextIn; } /** * @return The number of matched lines in this trace. */ public int getMatches() { return matches; } /** * @return whether this trace has terminated. */ public boolean isDone() { if (currentLineOld == -1 && currentLineNew == -1) { return true; } return false; } /** * @return The best possible number of matched lines for this trace. */ public int bestPossible() { int shortest = currentLineOld > currentLineNew ? currentLineNew : currentLineOld; return (matches + shortest + 1); //currentLine* is an index, so we must add one. } private void fork() { Edit copy = edit; if (edit != null && edit.getType() == Edit.ADD) { copy = edit.copy(); } Trace newTrace = new Trace(currentLineOld, currentLineNew, copy, matches, next); next = newTrace; makeAdd(); } /** * Step once in this trace. The power of this algorithm is the fact that * different traces are explored in parallel. This method is recursive while * it keeps finding matching lines. This is because finding matching lines makes * this trace much more possibly optimal. This behaviour is what Myers called * the "furthest reaching D-path" * <br/> * This step method steps backward through the files. This is for the simple reason * that when creating hunks, we want to create them in forward order, but we have to * visit the edits backwards from how we diffed them. Two backwards make a forward, * two negatives make a positive, and two wrongs make a right. * @param oldFile The old(first, from) file * @param newFile The new(second, to) file * @return whether this step forked. (needed for incrementation by the step controller) */ public boolean step(String[] oldFile, String[] newFile) { //This should never occur, because if this trace is done, it should have //already been called the best trace. However, defensive programming tells //me that I should not assume this. if (isDone()) { return false; } //We've reached the end of at least one file, the only possible trace is //exploring the other file. //We could just trace the rest of the remaining file here since we know what //it will be, but this trace is probably not the optimal trace if we his this //condition, so lets not waste effort on it. if (currentLineOld == -1) { makeAdd(); return false; } else if (currentLineNew == -1) { makeDelete(); return false; } String oldLine = oldFile[currentLineOld]; String newLine = newFile[currentLineNew]; if (oldLine.equals(newLine)) { makeMatch(); //recurse when we have a match, because this is more //likely the correct trace return step(oldFile, newFile); } //in order to avoid two equal traces (and therefore explode the //possible traces and thus memory used), once we start deleting, //we keep deleting. Since all traces that delete and then add //can be represented by ones that add and then delete, this is //computationally sound. if (edit != null && edit.getType() == Edit.DELETE) { makeDelete(); return false; } fork(); return true; } private void makeAdd() { makeEdit(Edit.ADD); currentLineNew--; } private void makeDelete() { makeEdit(Edit.DELETE); currentLineOld--; } private void makeMatch() { makeEdit(Edit.MATCH); matches++; currentLineNew--; currentLineOld--; } private void makeEdit(char c) { if (edit != null && edit.getType() == c) { edit.increment(); } else { edit = new Edit(c, edit); } } /** * Since the diff was performed backwards, "popping" the resulting edits from * the backwards tree gives them in forward order. * @param oldFile The old(first, from) file * @param newFile The new(second, to) file * @return A list of hunks representing the edit to make oldFile into newFile. */ public List<Hunk> createHunks(String[] oldFile, String[] newFile) { //start at the beginning of both files. currentLineOld = 0; currentLineNew = 0; int linesOld = 0; int linesNew = 0; List<Hunk> retval = new ArrayList<Hunk>(); Edit current = edit; while (current != null) { Hunk hunk; //first create the hunk. if (current.getType() == Edit.MATCH) { hunk = new MatchHunk(); linesOld = current.getNumber(); linesNew = current.getNumber(); } else if (current.getType() == Edit.ADD) { hunk = new InsertHunk(); linesOld = 0; linesNew = current.getNumber(); } else { //Delete hunk /* When diffing, we keep deleting once we started, which means that * going the opposite direction, there may be adds after deletes, but * not deletes after adds. * A change hunk is an add hunk and a delete hunk side by side. Here * is where we do that logic. */ if (current.getParent() != null && current.getParent().getType() == Edit.ADD) { hunk = new ChangeHunk(); linesOld = current.getNumber(); //this causes us to skip an edit. However, change hunks by //definition take two edits, so this is what we want. current = current.getParent(); linesNew = current.getNumber(); } else { hunk = new DeleteHunk(); linesOld = current.getNumber(); linesNew = 0; } } //now that we have a hunk put in the lines from the file. fillInHunk(hunk, oldFile, newFile, linesOld, linesNew); retval.add(hunk); //add hunk to return list current = current.getParent(); //increment } //while return retval; } private void fillInHunk(Hunk hunk, String[] oldFile, String[] newFile, int oldNum, int newNum) { hunk.setNewLines(createFileLines(newFile, currentLineNew, newNum)); hunk.setOldLines(createFileLines(oldFile, currentLineOld, oldNum)); //increment the current indexes, so that we don't visit the same lines. currentLineOld = currentLineOld + oldNum; currentLineNew = currentLineNew + newNum; } private FileLines createFileLines(String[] file, int fromLine, int numLines) { FileLines retval = new FileLines(); retval.setFromLine(fromLine + 1); //fromLine is an index, so it is one too small retval.setToLine(fromLine + numLines + 1); //fromLine is still an index for (int i = fromLine; i < fromLine + numLines; i++) { retval.addLine(file[i]); } return retval; } }