diff --git a/.idea/artifacts/VisualSapfor_jar.xml b/.idea/artifacts/VisualSapfor_jar.xml index 4cb563a3..6ebae0ef 100644 --- a/.idea/artifacts/VisualSapfor_jar.xml +++ b/.idea/artifacts/VisualSapfor_jar.xml @@ -23,6 +23,7 @@ + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index bcedc922..ac806ae4 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -7,9 +7,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/_VisualDVM/Visual/Windows/ComparisonForm.java b/src/_VisualDVM/Visual/Windows/ComparisonForm.java index 86c8d0b1..6b618046 100644 --- a/src/_VisualDVM/Visual/Windows/ComparisonForm.java +++ b/src/_VisualDVM/Visual/Windows/ComparisonForm.java @@ -9,20 +9,24 @@ import Common.Visual.Menus.VisualiserMenuBar; import Common.Visual.UI; import _VisualDVM.ProjectData.Files.UI.Editor.SPFEditor; import _VisualDVM.Utils; +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRowGenerator; import javafx.util.Pair; import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaHighlighter; import org.fife.ui.rtextarea.RTextScrollPane; import javax.swing.*; import java.util.LinkedHashMap; +import java.util.List; import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public abstract class ComparisonForm { public Class t; //класс объектов. //-->> public Vector lines = new Vector<>(); //строки с учетом/неучетом пробелов. для сравнения public Vector visible_lines = new Vector<>(); //строки с нетронутыми пробелами. для отображения //подсветка. - public LinkedHashMap> colors = new LinkedHashMap<>(); public RSyntaxTextAreaHighlighter slave_highlighter = null; //погонщик рабов protected JToolBar tools; protected JLabel lObjectName; @@ -39,8 +43,6 @@ public abstract class ComparisonForm { //-->> private JPanel content; private JPanel editorPanel; - private JButton bPrevious; - private JButton bNext; private JButton bCompare; private RTextScrollPane Scroll; //----- @@ -59,8 +61,6 @@ public abstract class ComparisonForm { t = t_in; this_ = this; slave = slave_in; - bPrevious.setVisible(isMaster()); - bNext.setVisible(isMaster()); Scroll.setLineNumbersEnabled(true); bApplyObject.addActionListener(e -> { ApplyObject(); @@ -85,24 +85,6 @@ public abstract class ComparisonForm { }); // slave.master = this; - bPrevious.addActionListener(e -> { - if (current_diff_line != CommonConstants.Nan) { - if (current_diff_line > 0) - current_diff_line--; - else - current_diff_line = colors.size() - 1; - ShowCurrentDiff(); - } - }); - bNext.addActionListener(e -> { - if (current_diff_line != CommonConstants.Nan) { - if (current_diff_line < colors.size() - 1) - current_diff_line++; - else - current_diff_line = 0; - ShowCurrentDiff(); - } - }); bCompare.addActionListener(e -> { DoComparePass(isReady() && slave.isReady()); }); @@ -146,7 +128,7 @@ public abstract class ComparisonForm { showObject(); } private void ShowCurrentDiff() { - Body.gotoLine_(colors.get(current_diff_line).getKey()); + // Body.gotoLine_(colors.get(current_diff_line).getKey()); } private void getLines() { lines.clear(); @@ -164,7 +146,6 @@ public abstract class ComparisonForm { protected void Compare() throws Exception { events_on = false; current_diff_line = CommonConstants.Nan; - colors.clear(); //----------------------------------------------------------------------------------------------- Body.setText(""); slave.Body.setText(""); @@ -175,62 +156,49 @@ public abstract class ComparisonForm { Vector t1 = new Vector<>(); Vector t2 = new Vector<>(); //------ - int old_j = 0; - int j = 0; - for (int i = 0; i < lines.size(); ++i) { - if (Utils.Contains(slave.lines, lines.get(i), old_j)) { - for (int k = old_j; k < slave.lines.size(); ++k) { - j = k; - if (Utils.CompareLines(lines.get(i), slave.lines.get(k))) { - j++; - t1.add(visible_lines.get(i)); - t2.add(slave.visible_lines.get(k)); - break; - } else { - t1.add("+"); - t2.add("+ " + slave.visible_lines.get(k)); - colors.put(d, new Pair(t2.size() - 1, true)); - ++d; - } - } - old_j = j; - } else { - //строки гарантированно нет. - t1.add("- " + visible_lines.get(i)); - t2.add("- " + visible_lines.get(i)); - colors.put(d, new Pair(t2.size() - 1, false)); - ++d; - } + DiffRowGenerator generator = DiffRowGenerator.create() + .showInlineDiffs(true) + .inlineDiffByWord(true) + .ignoreWhiteSpaces(true) + .oldTag(f -> "~") + .newTag(f -> "**") + .build(); + List rows = generator.generateDiffRows( + visible_lines, + slave.visible_lines); + + for (DiffRow row : rows) { + t1.add(row.getOldLine()); + t2.add(row.getNewLine()); } - //теперь граничное условие. если первый файл кончился а второй нет, его остаток это добавление. - for (int i = j; i < slave.lines.size(); ++i) { - t1.add("+"); - t2.add("+ " + slave.visible_lines.get(i)); - colors.put(d, new Pair(t2.size() - 1, true)); - ++d; - } - ///---------------- Body.setText(String.join("\n", t1)); slave.Body.setText(String.join("\n", t2)); Body.setCaretPosition(0); slave.Body.setCaretPosition(0); - //теперь покрас. - for (Integer diff_num : colors.keySet()) { - slave_highlighter.addHighlight( - slave.Body.getLineStartOffset(colors.get(diff_num).getKey()), - slave.Body.getLineEndOffset(colors.get(diff_num).getKey()), - colors.get(diff_num).getValue() ? - SPFEditor.GreenTextPainter : - SPFEditor.RedTextPainter + //-- + Pattern master_pattern = Pattern.compile("~.*~"); + Matcher master_matcher = master_pattern.matcher(Body.getText()); + while (master_matcher.find()) { + Body.getHighlighter().addHighlight( + master_matcher.start(), + master_matcher.end(), + SPFEditor.RedTextPainter + ); + } + Pattern slave_pattern = Pattern.compile("\\*.*\\*"); + Matcher slave_matcher = slave_pattern.matcher(slave.Body.getText()); + while (slave_matcher.find()) { + slave_highlighter.addHighlight( + slave_matcher.start(), + slave_matcher.end(), + SPFEditor.GreenTextPainter ); } - if (colors.size() > 0) current_diff_line = 0; events_on = true; } public void Show() throws Exception { events_on = false; current_diff_line = CommonConstants.Nan; - colors.clear(); //---------------------------------------------------------------------------------------------- Body.setText(""); slave.Body.setText(""); diff --git a/src/com/github/difflib/DiffUtils.java b/src/com/github/difflib/DiffUtils.java new file mode 100644 index 00000000..e0ac521f --- /dev/null +++ b/src/com/github/difflib/DiffUtils.java @@ -0,0 +1,228 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib; + +import com.github.difflib.algorithm.DiffAlgorithmFactory; +import com.github.difflib.algorithm.DiffAlgorithmI; +import com.github.difflib.algorithm.DiffAlgorithmListener; +import com.github.difflib.algorithm.myers.MyersDiff; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; +import com.github.difflib.patch.PatchFailedException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.BiPredicate; + +/** + * Utility class to implement the difference and patching engine. + */ +public final class DiffUtils { + + /** + * This factory generates the DEFAULT_DIFF algorithm for all these routines. + */ + static DiffAlgorithmFactory DEFAULT_DIFF = MyersDiff.factory(); + + /** + * Sets the default diff algorithm factory to be used by all diff routines. + * + * @param factory a {@link DiffAlgorithmFactory} representing the new default diff algorithm factory. + */ + public static void withDefaultDiffAlgorithmFactory(DiffAlgorithmFactory factory) { + DEFAULT_DIFF = factory; + } + + /** + * Computes the difference between two sequences of elements using the default diff algorithm. + * + * @param a generic representing the type of the elements to be compared. + * @param original a {@link List} representing the original sequence of elements. Must not be {@code null}. + * @param revised a {@link List} representing the revised sequence of elements. Must not be {@code null}. + * @param progress a {@link DiffAlgorithmListener} representing the progress listener. Can be {@code null}. + * @return The patch describing the difference between the original and revised sequences. Never {@code null}. + */ + public static Patch diff(List original, List revised, DiffAlgorithmListener progress) { + return DiffUtils.diff(original, revised, DEFAULT_DIFF.create(), progress); + } + + /** + * Computes the difference between two sequences of elements using the default diff algorithm. + * + * @param a generic representing the type of the elements to be compared. + * @param original a {@link List} representing the original sequence of elements. Must not be {@code null}. + * @param revised a {@link List} representing the revised sequence of elements. Must not be {@code null}. + * @return The patch describing the difference between the original and revised sequences. Never {@code null}. + */ + public static Patch diff(List original, List revised) { + return DiffUtils.diff(original, revised, DEFAULT_DIFF.create(), null); + } + + /** + * Computes the difference between two sequences of elements using the default diff algorithm. + * + * @param a generic representing the type of the elements to be compared. + * @param original a {@link List} representing the original sequence of elements. Must not be {@code null}. + * @param revised a {@link List} representing the revised sequence of elements. Must not be {@code null}. + * @param includeEqualParts a {@link boolean} representing whether to include equal parts in the resulting patch. + * @return The patch describing the difference between the original and revised sequences. Never {@code null}. + */ + public static Patch diff(List original, List revised, boolean includeEqualParts) { + return DiffUtils.diff(original, revised, DEFAULT_DIFF.create(), null, includeEqualParts); + } + + /** + * Computes the difference between two strings using the default diff algorithm. + * + * @param sourceText a {@link String} representing the original string. Must not be {@code null}. + * @param targetText a {@link String} representing the revised string. Must not be {@code null}. + * @param progress a {@link DiffAlgorithmListener} representing the progress listener. Can be {@code null}. + * @return The patch describing the difference between the original and revised strings. Never {@code null}. + */ + public static Patch diff(String sourceText, String targetText, + DiffAlgorithmListener progress) { + return DiffUtils.diff( + Arrays.asList(sourceText.split("\n")), + Arrays.asList(targetText.split("\n")), progress); + } + + /** + * Computes the difference between the original and revised list of elements + * with default diff algorithm + * + * @param source a {@link List} representing the original text. Must not be {@code null}. + * @param target a {@link List} representing the revised text. Must not be {@code null}. + * @param equalizer a {@link BiPredicate} representing the equalizer object to replace the default compare + * algorithm (Object.equals). If {@code null} the default equalizer of the + * default algorithm is used. + * @return The patch describing the difference between the original and + * revised sequences. Never {@code null}. + */ + public static Patch diff(List source, List target, + BiPredicate equalizer) { + if (equalizer != null) { + return DiffUtils.diff(source, target, + DEFAULT_DIFF.create(equalizer)); + } + return DiffUtils.diff(source, target, new MyersDiff<>()); + } + + public static Patch diff(List original, List revised, + DiffAlgorithmI algorithm, DiffAlgorithmListener progress) { + return diff(original, revised, algorithm, progress, false); + } + + /** + * Computes the difference between the original and revised list of elements + * with default diff algorithm + * + * @param original a {@link List} representing the original text. Must not be {@code null}. + * @param revised a {@link List} representing the revised text. Must not be {@code null}. + * @param algorithm a {@link DiffAlgorithmI} representing the diff algorithm. Must not be {@code null}. + * @param progress a {@link DiffAlgorithmListener} representing the diff algorithm listener. + * @param includeEqualParts Include equal data parts into the patch. + * @return The patch describing the difference between the original and + * revised sequences. Never {@code null}. + */ + public static Patch diff(List original, List revised, + DiffAlgorithmI algorithm, DiffAlgorithmListener progress, + boolean includeEqualParts) { + Objects.requireNonNull(original, "original must not be null"); + Objects.requireNonNull(revised, "revised must not be null"); + Objects.requireNonNull(algorithm, "algorithm must not be null"); + + return Patch.generate(original, revised, algorithm.computeDiff(original, revised, progress), includeEqualParts); + } + + + /** + * Computes the difference between the original and revised list of elements + * with default diff algorithm + * + * @param original a {@link List} representing the original text. Must not be {@code null}. + * @param revised a {@link List} representing the revised text. Must not be {@code null}. + * @param algorithm a {@link DiffAlgorithmI} representing the diff algorithm. Must not be {@code null}. + * @return The patch describing the difference between the original and + * revised sequences. Never {@code null}. + */ + public static Patch diff(List original, List revised, DiffAlgorithmI algorithm) { + return diff(original, revised, algorithm, null); + } + + /** + * Computes the difference between the given texts inline. This one uses the + * "trick" to make out of texts lists of characters, like DiffRowGenerator + * does and merges those changes at the end together again. + * + * @param original a {@link String} representing the original text. Must not be {@code null}. + * @param revised a {@link String} representing the revised text. Must not be {@code null}. + * @return The patch describing the difference between the original and + * revised sequences. Never {@code null}. + */ + public static Patch diffInline(String original, String revised) { + List origList = new ArrayList<>(); + List revList = new ArrayList<>(); + for (Character character : original.toCharArray()) { + origList.add(character.toString()); + } + for (Character character : revised.toCharArray()) { + revList.add(character.toString()); + } + Patch patch = DiffUtils.diff(origList, revList); + for (AbstractDelta delta : patch.getDeltas()) { + delta.getSource().setLines(compressLines(delta.getSource().getLines(), "")); + delta.getTarget().setLines(compressLines(delta.getTarget().getLines(), "")); + } + return patch; + } + + /** + * Applies the given patch to the original list and returns the revised list. + * + * @param original a {@link List} representing the original list. + * @param patch a {@link List} representing the patch to apply. + * @return the revised list. + * @throws PatchFailedException if the patch cannot be applied. + */ + public static List patch(List original, Patch patch) + throws PatchFailedException { + return patch.applyTo(original); + } + + /** + * Applies the given patch to the revised list and returns the original list. + * + * @param revised a {@link List} representing the revised list. + * @param patch a {@link Patch} representing the patch to apply. + * @return the original list. + * @throws PatchFailedException if the patch cannot be applied. + */ + public static List unpatch(List revised, Patch patch) { + return patch.restore(revised); + } + + private static List compressLines(List lines, String delimiter) { + if (lines.isEmpty()) { + return Collections.emptyList(); + } + return Collections.singletonList(String.join(delimiter, lines)); + } + + private DiffUtils() { + } +} diff --git a/src/com/github/difflib/UnifiedDiffUtils.java b/src/com/github/difflib/UnifiedDiffUtils.java new file mode 100644 index 00000000..94786b6c --- /dev/null +++ b/src/com/github/difflib/UnifiedDiffUtils.java @@ -0,0 +1,467 @@ +/* + * Copyright 2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib; + +import com.github.difflib.patch.ChangeDelta; +import com.github.difflib.patch.Chunk; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * + * @author toben + */ +public final class UnifiedDiffUtils { + + private static final Pattern UNIFIED_DIFF_CHUNK_REGEXP = Pattern + .compile("^@@\\s+-(\\d+)(?:,(\\d+))?\\s+\\+(\\d+)(?:,(\\d+))?\\s+@@.*$"); + private static final String NULL_FILE_INDICATOR = "/dev/null"; + + /** + * Parse the given text in unified format and creates the list of deltas for it. + * + * @param diff the text in unified format + * @return the patch with deltas. + */ + public static Patch parseUnifiedDiff(List diff) { + boolean inPrelude = true; + List rawChunk = new ArrayList<>(); + Patch patch = new Patch<>(); + + int old_ln = 0; + int new_ln = 0; + String tag; + String rest; + for (String line : diff) { + // Skip leading lines until after we've seen one starting with '+++' + if (inPrelude) { + if (line.startsWith("+++")) { + inPrelude = false; + } + continue; + } + Matcher m = UNIFIED_DIFF_CHUNK_REGEXP.matcher(line); + if (m.find()) { + // Process the lines in the previous chunk + processLinesInPrevChunk(rawChunk, patch, old_ln, new_ln); + // Parse the @@ header + old_ln = m.group(1) == null ? 1 : Integer.parseInt(m.group(1)); + new_ln = m.group(3) == null ? 1 : Integer.parseInt(m.group(3)); + + if (old_ln == 0) { + old_ln = 1; + } + if (new_ln == 0) { + new_ln = 1; + } + } else { + if (line.length() > 0) { + tag = line.substring(0, 1); + rest = line.substring(1); + if (" ".equals(tag) || "+".equals(tag) || "-".equals(tag)) { + rawChunk.add(new String[]{tag, rest}); + } + } else { + rawChunk.add(new String[]{" ", ""}); + } + } + } + + // Process the lines in the last chunk + processLinesInPrevChunk(rawChunk, patch, old_ln, new_ln); + + return patch; + } + + private static void processLinesInPrevChunk(List rawChunk, Patch patch, int old_ln, int new_ln) { + String tag; + String rest; + if (!rawChunk.isEmpty()) { + List oldChunkLines = new ArrayList<>(); + List newChunkLines = new ArrayList<>(); + + List removePosition = new ArrayList<>(); + List addPosition = new ArrayList<>(); + int removeNum = 0; + int addNum = 0; + for (String[] raw_line : rawChunk) { + tag = raw_line[0]; + rest = raw_line[1]; + if (" ".equals(tag) || "-".equals(tag)) { + removeNum++; + oldChunkLines.add(rest); + if ("-".equals(tag)) { + removePosition.add(old_ln - 1 + removeNum); + } + } + if (" ".equals(tag) || "+".equals(tag)) { + addNum++; + newChunkLines.add(rest); + if ("+".equals(tag)) { + addPosition.add(new_ln - 1 + addNum); + } + } + } + patch.addDelta(new ChangeDelta<>(new Chunk<>( + old_ln - 1, oldChunkLines, removePosition), new Chunk<>( + new_ln - 1, newChunkLines, addPosition))); + rawChunk.clear(); + } + } + + /** + * generateUnifiedDiff takes a Patch and some other arguments, returning the Unified Diff format + * text representing the Patch. Author: Bill James (tankerbay@gmail.com). + * + * @param originalFileName - Filename of the original (unrevised file) + * @param revisedFileName - Filename of the revised file + * @param originalLines - Lines of the original file + * @param patch - Patch created by the diff() function + * @param contextSize - number of lines of context output around each difference in the file. + * @return List of strings representing the Unified Diff representation of the Patch argument. + */ + public static List generateUnifiedDiff(String originalFileName, + String revisedFileName, List originalLines, Patch patch, + int contextSize) { + if (!patch.getDeltas().isEmpty()) { + List ret = new ArrayList<>(); + ret.add("--- " + Optional.ofNullable(originalFileName).orElse(NULL_FILE_INDICATOR)); + ret.add("+++ " + Optional.ofNullable(revisedFileName).orElse(NULL_FILE_INDICATOR)); + + List> patchDeltas = new ArrayList<>( + patch.getDeltas()); + + // code outside the if block also works for single-delta issues. + List> deltas = new ArrayList<>(); // current + // list + // of + // Delta's to + // process + AbstractDelta delta = patchDeltas.get(0); + deltas.add(delta); // add the first Delta to the current set + // if there's more than 1 Delta, we may need to output them together + if (patchDeltas.size() > 1) { + for (int i = 1; i < patchDeltas.size(); i++) { + int position = delta.getSource().getPosition(); // store + // the + // current + // position + // of + // the first Delta + + // Check if the next Delta is too close to the current + // position. + // And if it is, add it to the current set + AbstractDelta nextDelta = patchDeltas.get(i); + if ((position + delta.getSource().size() + contextSize) >= (nextDelta + .getSource().getPosition() - contextSize)) { + deltas.add(nextDelta); + } else { + // if it isn't, output the current set, + // then create a new set and add the current Delta to + // it. + List curBlock = processDeltas(originalLines, + deltas, contextSize, false); + ret.addAll(curBlock); + deltas.clear(); + deltas.add(nextDelta); + } + delta = nextDelta; + } + + } + // don't forget to process the last set of Deltas + List curBlock = processDeltas(originalLines, deltas, + contextSize, patchDeltas.size() == 1 && originalFileName == null); + ret.addAll(curBlock); + return ret; + } + return new ArrayList<>(); + } + + /** + * processDeltas takes a list of Deltas and outputs them together in a single block of + * Unified-Diff-format text. Author: Bill James (tankerbay@gmail.com). + * + * @param origLines - the lines of the original file + * @param deltas - the Deltas to be output as a single block + * @param contextSize - the number of lines of context to place around block + * @return + */ + private static List processDeltas(List origLines, + List> deltas, int contextSize, boolean newFile) { + List buffer = new ArrayList<>(); + int origTotal = 0; // counter for total lines output from Original + int revTotal = 0; // counter for total lines output from Original + int line; + + AbstractDelta curDelta = deltas.get(0); + int origStart; + if (newFile) { + origStart = 0; + } else { + // NOTE: +1 to overcome the 0-offset Position + origStart = curDelta.getSource().getPosition() + 1 - contextSize; + if (origStart < 1) { + origStart = 1; + } + } + + int revStart = curDelta.getTarget().getPosition() + 1 - contextSize; + if (revStart < 1) { + revStart = 1; + } + + // find the start of the wrapper context code + int contextStart = curDelta.getSource().getPosition() - contextSize; + if (contextStart < 0) { + contextStart = 0; // clamp to the start of the file + } + + // output the context before the first Delta + for (line = contextStart; line < curDelta.getSource().getPosition(); line++) { // + buffer.add(" " + origLines.get(line)); + origTotal++; + revTotal++; + } + + // output the first Delta + buffer.addAll(getDeltaText(curDelta)); + origTotal += curDelta.getSource().getLines().size(); + revTotal += curDelta.getTarget().getLines().size(); + + int deltaIndex = 1; + while (deltaIndex < deltas.size()) { // for each of the other Deltas + AbstractDelta nextDelta = deltas.get(deltaIndex); + int intermediateStart = curDelta.getSource().getPosition() + + curDelta.getSource().getLines().size(); + for (line = intermediateStart; line < nextDelta.getSource() + .getPosition(); line++) { + // output the code between the last Delta and this one + buffer.add(" " + origLines.get(line)); + origTotal++; + revTotal++; + } + buffer.addAll(getDeltaText(nextDelta)); // output the Delta + origTotal += nextDelta.getSource().getLines().size(); + revTotal += nextDelta.getTarget().getLines().size(); + curDelta = nextDelta; + deltaIndex++; + } + + // Now output the post-Delta context code, clamping the end of the file + contextStart = curDelta.getSource().getPosition() + + curDelta.getSource().getLines().size(); + for (line = contextStart; (line < (contextStart + contextSize)) + && (line < origLines.size()); line++) { + buffer.add(" " + origLines.get(line)); + origTotal++; + revTotal++; + } + + // Create and insert the block header, conforming to the Unified Diff + // standard + StringBuilder header = new StringBuilder(); + header.append("@@ -"); + header.append(origStart); + header.append(","); + header.append(origTotal); + header.append(" +"); + header.append(revStart); + header.append(","); + header.append(revTotal); + header.append(" @@"); + buffer.add(0, header.toString()); + + return buffer; + } + + /** + * getDeltaText returns the lines to be added to the Unified Diff text from the Delta parameter. Author: Bill James (tankerbay@gmail.com). + * + * @param delta - the Delta to output + * @return list of String lines of code. + */ + private static List getDeltaText(AbstractDelta delta) { + List buffer = new ArrayList<>(); + for (String line : delta.getSource().getLines()) { + buffer.add("-" + line); + } + for (String line : delta.getTarget().getLines()) { + buffer.add("+" + line); + } + return buffer; + } + + private UnifiedDiffUtils() { + } + + /** + * Compare the differences between two files and return to the original file and diff format + * + * (This method compares the original file with the comparison file to obtain a diff, and inserts the diff into the corresponding position of the original file. + * You can see all the differences and unmodified places from the original file. + * Also, this will be very easy and useful for making side-by-side comparison display applications, + * for example, if you use diff2html (https://github.com/rtfpessoa/diff2html#usage) + * Wait for tools to display your differences on html pages, you only need to insert the return value into your js code) + * + * @param original Original file content + * @param revised revised file content + * + */ + public static List generateOriginalAndDiff(List original, List revised) { + return generateOriginalAndDiff(original, revised, null, null); + } + + + /** + * Compare the differences between two files and return to the original file and diff format + * + * (This method compares the original file with the comparison file to obtain a diff, and inserts the diff into the corresponding position of the original file. + * You can see all the differences and unmodified places from the original file. + * Also, this will be very easy and useful for making side-by-side comparison display applications, + * for example, if you use diff2html (https://github.com/rtfpessoa/diff2html#usage) + * Wait for tools to display your differences on html pages, you only need to insert the return value into your js code) + * + * @param original Original file content + * @param revised revised file content + * @param originalFileName Original file name + * @param revisedFileName revised file name + */ + public static List generateOriginalAndDiff(List original, List revised, String originalFileName, String revisedFileName) { + String originalFileNameTemp = originalFileName; + String revisedFileNameTemp = revisedFileName; + if (originalFileNameTemp == null) { + originalFileNameTemp = "original"; + } + if (revisedFileNameTemp == null) { + revisedFileNameTemp = "revised"; + } + Patch patch = DiffUtils.diff(original, revised); + List unifiedDiff = generateUnifiedDiff(originalFileNameTemp, revisedFileNameTemp, original, patch, 0); + if (unifiedDiff.isEmpty()) { + unifiedDiff.add("--- " + originalFileNameTemp); + unifiedDiff.add("+++ " + revisedFileNameTemp); + unifiedDiff.add("@@ -0,0 +0,0 @@"); + } else if (unifiedDiff.size() >= 3 && !unifiedDiff.get(2).contains("@@ -1,")) { + unifiedDiff.set(1, unifiedDiff.get(1)); + unifiedDiff.add(2, "@@ -0,0 +0,0 @@"); + } + List originalWithPrefix = original.stream().map(v -> " " + v).collect(Collectors.toList()); + return insertOrig(originalWithPrefix, unifiedDiff); + } + + //Insert the diff format to the original file + private static List insertOrig(List original, List unifiedDiff) { + List result = new ArrayList<>(); + List> diffList = new ArrayList<>(); + List diff = new ArrayList<>(); + for (int i = 0; i < unifiedDiff.size(); i++) { + String u = unifiedDiff.get(i); + if (u.startsWith("@@") && !"@@ -0,0 +0,0 @@".equals(u) && !u.contains("@@ -1,")) { + List twoList = new ArrayList<>(); + twoList.addAll(diff); + diffList.add(twoList); + diff.clear(); + diff.add(u); + continue; + } + if (i == unifiedDiff.size() - 1) { + diff.add(u); + List twoList = new ArrayList<>(); + twoList.addAll(diff); + diffList.add(twoList); + diff.clear(); + break; + } + diff.add(u); + } + insertOrig(diffList, result, original); + return result; + } + + //Insert the diff format to the original file + private static void insertOrig(List> diffList, List result, List original) { + for (int i = 0; i < diffList.size(); i++) { + List diff = diffList.get(i); + List nexDiff = i == diffList.size() - 1 ? null : diffList.get(i + 1); + String simb = i == 0 ? diff.get(2) : diff.get(0); + String nexSimb = nexDiff == null ? null : nexDiff.get(0); + insert(result, diff); + Map map = getRowMap(simb); + if (null != nexSimb) { + Map nexMap = getRowMap(nexSimb); + int start = 0; + if (map.get("orgRow") != 0) { + start = map.get("orgRow") + map.get("orgDel") - 1; + } + int end = nexMap.get("revRow") - 2; + insert(result, getOrigList(original, start, end)); + } + int start = map.get("orgRow") + map.get("orgDel") - 1; + start = start == -1 ? 0 : start; + if (simb.contains("@@ -1,") && null == nexSimb && map.get("orgDel") != original.size()) { + insert(result, getOrigList(original, start, original.size() - 1)); + } else if (null == nexSimb && (map.get("orgRow") + map.get("orgDel") - 1) < original.size()) { + insert(result, getOrigList(original, start, original.size() - 1)); + } + } + } + + //Insert the unchanged content in the source file into result + private static void insert(List result, List noChangeContent) { + for (String ins : noChangeContent) { + result.add(ins); + } + } + + //Parse the line containing @@ to get the modified line number to delete or add a few lines + private static Map getRowMap(String str) { + Map map = new HashMap<>(); + if (str.startsWith("@@")) { + String[] sp = str.split(" "); + String org = sp[1]; + String[] orgSp = org.split(","); + map.put("orgRow", Integer.valueOf(orgSp[0].substring(1))); + map.put("orgDel", Integer.valueOf(orgSp[1])); + String[] revSp = org.split(","); + map.put("revRow", Integer.valueOf(revSp[0].substring(1))); + map.put("revAdd", Integer.valueOf(revSp[1])); + } + return map; + } + + //Get the specified part of the line from the original file + private static List getOrigList(List originalWithPrefix, int start, int end) { + List list = new ArrayList<>(); + if (originalWithPrefix.size() >= 1 && start <= end && end < originalWithPrefix.size()) { + int startTemp = start; + for (; startTemp <= end; startTemp++) { + list.add(originalWithPrefix.get(startTemp)); + } + } + return list; + } +} diff --git a/src/com/github/difflib/algorithm/Change.java b/src/com/github/difflib/algorithm/Change.java new file mode 100644 index 00000000..9b6f1dfe --- /dev/null +++ b/src/com/github/difflib/algorithm/Change.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.algorithm; + +import com.github.difflib.patch.DeltaType; + +/** + * + * @author Tobias Warneke + */ +public class Change { + + public final DeltaType deltaType; + public final int startOriginal; + public final int endOriginal; + public final int startRevised; + public final int endRevised; + + public Change(DeltaType deltaType, int startOriginal, int endOriginal, int startRevised, int endRevised) { + this.deltaType = deltaType; + this.startOriginal = startOriginal; + this.endOriginal = endOriginal; + this.startRevised = startRevised; + this.endRevised = endRevised; + } + + public Change withEndOriginal(int endOriginal) { + return new Change(deltaType, startOriginal, endOriginal, startRevised, endRevised); + } + + public Change withEndRevised(int endRevised) { + return new Change(deltaType, startOriginal, endOriginal, startRevised, endRevised); + } +} diff --git a/src/com/github/difflib/algorithm/DiffAlgorithmFactory.java b/src/com/github/difflib/algorithm/DiffAlgorithmFactory.java new file mode 100644 index 00000000..7e5205cd --- /dev/null +++ b/src/com/github/difflib/algorithm/DiffAlgorithmFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.algorithm; + +import java.util.function.BiPredicate; + +/** + * Tool to create new instances of a diff algorithm. This one is only needed at the moment to + * set DiffUtils default diff algorithm. + * @author tw + */ +public interface DiffAlgorithmFactory { + DiffAlgorithmI create(); + + DiffAlgorithmI create(BiPredicate equalizer); +} diff --git a/src/com/github/difflib/algorithm/DiffAlgorithmI.java b/src/com/github/difflib/algorithm/DiffAlgorithmI.java new file mode 100644 index 00000000..117656e4 --- /dev/null +++ b/src/com/github/difflib/algorithm/DiffAlgorithmI.java @@ -0,0 +1,50 @@ +/* + * Copyright 2018 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.algorithm; + +import java.util.Arrays; +import java.util.List; + +/** + * Interface of a diff algorithm. + * + * @author Tobias Warneke (t.warneke@gmx.net) + * @param type of data that is diffed. + */ +public interface DiffAlgorithmI { + + /** + * Computes the changeset to patch the source list to the target list. + * + * @param source source data + * @param target target data + * @param progress progress listener + * @return + */ + List computeDiff(List source, List target, DiffAlgorithmListener progress); + + /** + * Simple extension to compute a changeset using arrays. + * + * @param source + * @param target + * @param progress + * @return + */ + default List computeDiff(T[] source, T[] target, DiffAlgorithmListener progress) { + return computeDiff(Arrays.asList(source), Arrays.asList(target), progress); + } +} diff --git a/src/com/github/difflib/algorithm/DiffAlgorithmListener.java b/src/com/github/difflib/algorithm/DiffAlgorithmListener.java new file mode 100644 index 00000000..37d51813 --- /dev/null +++ b/src/com/github/difflib/algorithm/DiffAlgorithmListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.algorithm; + +/** + * + * @author Tobias Warneke (t.warneke@gmx.net) + */ +public interface DiffAlgorithmListener { + void diffStart(); + + /** + * This is a step within the diff algorithm. Due to different implementations the value + * is not strict incrementing to the max and is not garantee to reach the max. It could + * stop before. + * @param value + * @param max + */ + void diffStep(int value, int max); + void diffEnd(); +} diff --git a/src/com/github/difflib/algorithm/myers/MyersDiff.java b/src/com/github/difflib/algorithm/myers/MyersDiff.java new file mode 100644 index 00000000..2517de46 --- /dev/null +++ b/src/com/github/difflib/algorithm/myers/MyersDiff.java @@ -0,0 +1,200 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.algorithm.myers; + +import com.github.difflib.algorithm.Change; +import com.github.difflib.algorithm.DiffAlgorithmFactory; +import com.github.difflib.algorithm.DiffAlgorithmI; +import com.github.difflib.algorithm.DiffAlgorithmListener; +import com.github.difflib.patch.DeltaType; +import com.github.difflib.patch.Patch; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.BiPredicate; + +/** + * A clean-room implementation of Eugene Myers greedy differencing algorithm. + */ +public final class MyersDiff implements DiffAlgorithmI { + + private final BiPredicate equalizer; + + public MyersDiff() { + equalizer = Object::equals; + } + + public MyersDiff(final BiPredicate equalizer) { + Objects.requireNonNull(equalizer, "equalizer must not be null"); + this.equalizer = equalizer; + } + + /** + * {@inheritDoc} + * + * Return empty diff if get the error while procession the difference. + */ + @Override + public List computeDiff(final List source, final List target, DiffAlgorithmListener progress) { + Objects.requireNonNull(source, "source list must not be null"); + Objects.requireNonNull(target, "target list must not be null"); + + if (progress != null) { + progress.diffStart(); + } + PathNode path = buildPath(source, target, progress); + List result = buildRevision(path, source, target); + if (progress != null) { + progress.diffEnd(); + } + return result; + } + + /** + * Computes the minimum diffpath that expresses de differences between the + * original and revised sequences, according to Gene Myers differencing + * algorithm. + * + * @param orig The original sequence. + * @param rev The revised sequence. + * @return A minimum {@link PathNode Path} accross the differences graph. + * @throws DifferentiationFailedException if a diff path could not be found. + */ + private PathNode buildPath(final List orig, final List rev, DiffAlgorithmListener progress) { + Objects.requireNonNull(orig, "original sequence is null"); + Objects.requireNonNull(rev, "revised sequence is null"); + + // these are local constants + final int N = orig.size(); + final int M = rev.size(); + + final int MAX = N + M + 1; + final int size = 1 + 2 * MAX; + final int middle = size / 2; + final PathNode diagonal[] = new PathNode[size]; + + diagonal[middle + 1] = new PathNode(0, -1, true, true, null); + for (int d = 0; d < MAX; d++) { + if (progress != null) { + progress.diffStep(d, MAX); + } + for (int k = -d; k <= d; k += 2) { + final int kmiddle = middle + k; + final int kplus = kmiddle + 1; + final int kminus = kmiddle - 1; + PathNode prev; + int i; + + if ((k == -d) || (k != d && diagonal[kminus].i < diagonal[kplus].i)) { + i = diagonal[kplus].i; + prev = diagonal[kplus]; + } else { + i = diagonal[kminus].i + 1; + prev = diagonal[kminus]; + } + + diagonal[kminus] = null; // no longer used + + int j = i - k; + + PathNode node = new PathNode(i, j, false, false, prev); + + while (i < N && j < M && equalizer.test(orig.get(i), rev.get(j))) { + i++; + j++; + } + + if (i != node.i) { + node = new PathNode(i, j, true, false, node); + } + + diagonal[kmiddle] = node; + + if (i >= N && j >= M) { + return diagonal[kmiddle]; + } + } + diagonal[middle + d - 1] = null; + } + // According to Myers, this cannot happen + throw new IllegalStateException("could not find a diff path"); + } + + /** + * Constructs a {@link Patch} from a difference path. + * + * @param actualPath The path. + * @param orig The original sequence. + * @param rev The revised sequence. + * @return A {@link Patch} script corresponding to the path. + * @throws DifferentiationFailedException if a {@link Patch} could not be + * built from the given path. + */ + private List buildRevision(PathNode actualPath, List orig, List rev) { + Objects.requireNonNull(actualPath, "path is null"); + Objects.requireNonNull(orig, "original sequence is null"); + Objects.requireNonNull(rev, "revised sequence is null"); + + PathNode path = actualPath; + List changes = new ArrayList<>(); + if (path.isSnake()) { + path = path.prev; + } + while (path != null && path.prev != null && path.prev.j >= 0) { + if (path.isSnake()) { + throw new IllegalStateException("bad diffpath: found snake when looking for diff"); + } + int i = path.i; + int j = path.j; + + path = path.prev; + int ianchor = path.i; + int janchor = path.j; + + if (ianchor == i && janchor != j) { + changes.add(new Change(DeltaType.INSERT, ianchor, i, janchor, j)); + } else if (ianchor != i && janchor == j) { + changes.add(new Change(DeltaType.DELETE, ianchor, i, janchor, j)); + } else { + changes.add(new Change(DeltaType.CHANGE, ianchor, i, janchor, j)); + } + + if (path.isSnake()) { + path = path.prev; + } + } + return changes; + } + + /** + * Factory to create instances of this specific diff algorithm. + */ + public static DiffAlgorithmFactory factory() { + return new DiffAlgorithmFactory() { + @Override + public DiffAlgorithmI + create() { + return new MyersDiff<>(); + } + + @Override + public DiffAlgorithmI + create(BiPredicate < T, T > equalizer) { + return new MyersDiff<>(equalizer); + } + }; + } +} diff --git a/src/com/github/difflib/algorithm/myers/MyersDiffWithLinearSpace.java b/src/com/github/difflib/algorithm/myers/MyersDiffWithLinearSpace.java new file mode 100644 index 00000000..ca4114c4 --- /dev/null +++ b/src/com/github/difflib/algorithm/myers/MyersDiffWithLinearSpace.java @@ -0,0 +1,244 @@ +/* + * Copyright 2021 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.algorithm.myers; + +import com.github.difflib.algorithm.Change; +import com.github.difflib.algorithm.DiffAlgorithmFactory; +import com.github.difflib.algorithm.DiffAlgorithmI; +import com.github.difflib.algorithm.DiffAlgorithmListener; +import com.github.difflib.patch.DeltaType; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Consumer; + +/** + * + * @author tw + */ +public class MyersDiffWithLinearSpace implements DiffAlgorithmI { + + private final BiPredicate equalizer; + + public MyersDiffWithLinearSpace() { + equalizer = Object::equals; + } + + public MyersDiffWithLinearSpace(final BiPredicate equalizer) { + Objects.requireNonNull(equalizer, "equalizer must not be null"); + this.equalizer = equalizer; + } + + @Override + public List computeDiff(List source, List target, DiffAlgorithmListener progress) { + Objects.requireNonNull(source, "source list must not be null"); + Objects.requireNonNull(target, "target list must not be null"); + + if (progress != null) { + progress.diffStart(); + } + + DiffData data = new DiffData(source, target); + + int maxIdx = source.size() + target.size(); + + buildScript(data, 0, source.size(), 0, target.size(), idx -> { + if (progress != null) { + progress.diffStep(idx, maxIdx); + } + }); + + if (progress != null) { + progress.diffEnd(); + } + return data.script; + } + + private void buildScript(DiffData data, int start1, int end1, int start2, int end2, Consumer progress) { + if (progress != null) { + progress.accept((end1 - start1) / 2 + (end2 - start2) / 2); + } + final Snake middle = getMiddleSnake(data, start1, end1, start2, end2); + if (middle == null + || middle.start == end1 && middle.diag == end1 - end2 + || middle.end == start1 && middle.diag == start1 - start2) { + int i = start1; + int j = start2; + while (i < end1 || j < end2) { + if (i < end1 && j < end2 && equalizer.test(data.source.get(i), data.target.get(j))) { + //script.append(new KeepCommand<>(left.charAt(i))); + ++i; + ++j; + } else { + //TODO: compress these commands. + if (end1 - start1 > end2 - start2) { + //script.append(new DeleteCommand<>(left.charAt(i))); + if (data.script.isEmpty() + || data.script.get(data.script.size() - 1).endOriginal != i + || data.script.get(data.script.size() - 1).deltaType != DeltaType.DELETE) { + data.script.add(new Change(DeltaType.DELETE, i, i + 1, j, j)); + } else { + data.script.set(data.script.size() - 1, data.script.get(data.script.size() - 1).withEndOriginal(i + 1)); + } + ++i; + } else { + if (data.script.isEmpty() + || data.script.get(data.script.size() - 1).endRevised != j + || data.script.get(data.script.size() - 1).deltaType != DeltaType.INSERT) { + data.script.add(new Change(DeltaType.INSERT, i, i, j, j + 1)); + } else { + data.script.set(data.script.size() - 1, data.script.get(data.script.size() - 1).withEndRevised(j + 1)); + } + ++j; + } + } + } + } else { + buildScript(data, start1, middle.start, start2, middle.start - middle.diag, progress); + buildScript(data, middle.end, end1, middle.end - middle.diag, end2, progress); + } + } + + private Snake getMiddleSnake(DiffData data, int start1, int end1, int start2, int end2) { + final int m = end1 - start1; + final int n = end2 - start2; + if (m == 0 || n == 0) { + return null; + } + + final int delta = m - n; + final int sum = n + m; + final int offset = (sum % 2 == 0 ? sum : sum + 1) / 2; + data.vDown[1 + offset] = start1; + data.vUp[1 + offset] = end1 + 1; + + for (int d = 0; d <= offset; ++d) { + // Down + for (int k = -d; k <= d; k += 2) { + // First step + + final int i = k + offset; + if (k == -d || k != d && data.vDown[i - 1] < data.vDown[i + 1]) { + data.vDown[i] = data.vDown[i + 1]; + } else { + data.vDown[i] = data.vDown[i - 1] + 1; + } + + int x = data.vDown[i]; + int y = x - start1 + start2 - k; + + while (x < end1 && y < end2 && equalizer.test(data.source.get(x), data.target.get(y))) { + data.vDown[i] = ++x; + ++y; + } + // Second step + if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { + if (data.vUp[i - delta] <= data.vDown[i]) { + return buildSnake(data, data.vUp[i - delta], k + start1 - start2, end1, end2); + } + } + } + + // Up + for (int k = delta - d; k <= delta + d; k += 2) { + // First step + final int i = k + offset - delta; + if (k == delta - d + || k != delta + d && data.vUp[i + 1] <= data.vUp[i - 1]) { + data.vUp[i] = data.vUp[i + 1] - 1; + } else { + data.vUp[i] = data.vUp[i - 1]; + } + + int x = data.vUp[i] - 1; + int y = x - start1 + start2 - k; + while (x >= start1 && y >= start2 && equalizer.test(data.source.get(x), data.target.get(y))) { + data.vUp[i] = x--; + y--; + } + // Second step + if (delta % 2 == 0 && -d <= k && k <= d) { + if (data.vUp[i] <= data.vDown[i + delta]) { + return buildSnake(data, data.vUp[i], k + start1 - start2, end1, end2); + } + } + } + } + + // According to Myers, this cannot happen + throw new IllegalStateException("could not find a diff path"); + } + + private Snake buildSnake(DiffData data, final int start, final int diag, final int end1, final int end2) { + int end = start; + while (end - diag < end2 && end < end1 && equalizer.test(data.source.get(end), data.target.get(end - diag))) { + ++end; + } + return new Snake(start, end, diag); + } + + private class DiffData { + + final int size; + final int[] vDown; + final int[] vUp; + final List script; + final List source; + final List target; + + public DiffData(List source, List target) { + this.source = source; + this.target = target; + size = source.size() + target.size() + 2; + vDown = new int[size]; + vUp = new int[size]; + script = new ArrayList<>(); + } + } + + private class Snake { + + final int start; + final int end; + final int diag; + + public Snake(final int start, final int end, final int diag) { + this.start = start; + this.end = end; + this.diag = diag; + } + } + + /** + * Factory to create instances of this specific diff algorithm. + */ + public static DiffAlgorithmFactory factory() { + return new DiffAlgorithmFactory() { + @Override + public DiffAlgorithmI + create() { + return new MyersDiffWithLinearSpace<>(); + } + + @Override + public DiffAlgorithmI + create(BiPredicate < T, T > equalizer) { + return new MyersDiffWithLinearSpace<>(equalizer); + } + }; + } +} diff --git a/src/com/github/difflib/algorithm/myers/PathNode.java b/src/com/github/difflib/algorithm/myers/PathNode.java new file mode 100644 index 00000000..fe8fd03a --- /dev/null +++ b/src/com/github/difflib/algorithm/myers/PathNode.java @@ -0,0 +1,110 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.algorithm.myers; + +/** + * A node in a diffpath. + * + * @author Juanco Anez + */ +public final class PathNode { + + /** + * Position in the original sequence. + */ + public final int i; + /** + * Position in the revised sequence. + */ + public final int j; + /** + * The previous node in the path. + */ + public final PathNode prev; + + public final boolean snake; + + public final boolean bootstrap; + + /** + * Concatenates a new path node with an existing diffpath. + * + * @param i The position in the original sequence for the new node. + * @param j The position in the revised sequence for the new node. + * @param prev The previous node in the path. + */ + public PathNode(int i, int j, boolean snake, boolean bootstrap, PathNode prev) { + this.i = i; + this.j = j; + this.bootstrap = bootstrap; + if (snake) { + this.prev = prev; + } else { + this.prev = prev == null ? null : prev.previousSnake(); + } + this.snake = snake; + } + + public boolean isSnake() { + return snake; + } + + /** + * Is this a bootstrap node? + *

+ * In bottstrap nodes one of the two corrdinates is less than zero. + * + * @return tru if this is a bootstrap node. + */ + public boolean isBootstrap() { + return bootstrap; + } + + /** + * Skips sequences of {@link PathNode PathNodes} until a snake or bootstrap node is found, or the end of the + * path is reached. + * + * @return The next first {@link PathNode} or bootstrap node in the path, or null if none found. + */ + public final PathNode previousSnake() { + if (isBootstrap()) { + return null; + } + if (!isSnake() && prev != null) { + return prev.previousSnake(); + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuilder buf = new StringBuilder("["); + PathNode node = this; + while (node != null) { + buf.append("("); + buf.append(node.i); + buf.append(","); + buf.append(node.j); + buf.append(")"); + node = node.prev; + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/src/com/github/difflib/patch/AbstractDelta.java b/src/com/github/difflib/patch/AbstractDelta.java new file mode 100644 index 00000000..f74f62ca --- /dev/null +++ b/src/com/github/difflib/patch/AbstractDelta.java @@ -0,0 +1,116 @@ +/* + * Copyright 2018 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * Abstract delta between a source and a target. + * @author Tobias Warneke (t.warneke@gmx.net) + */ +public abstract class AbstractDelta implements Serializable { + private final Chunk source; + private final Chunk target; + private final DeltaType type; + + public AbstractDelta(DeltaType type, Chunk source, Chunk target) { + Objects.requireNonNull(source); + Objects.requireNonNull(target); + Objects.requireNonNull(type); + this.type = type; + this.source = source; + this.target = target; + } + + public Chunk getSource() { + return source; + } + + public Chunk getTarget() { + return target; + } + + public DeltaType getType() { + return type; + } + + /** + * Verify the chunk of this delta, to fit the target. + * @param target + * @throws PatchFailedException + */ + protected VerifyChunk verifyChunkToFitTarget(List target) throws PatchFailedException { + return getSource().verifyChunk(target); + } + + protected VerifyChunk verifyAndApplyTo(List target) throws PatchFailedException { + final VerifyChunk verify = verifyChunkToFitTarget(target); + if (verify == VerifyChunk.OK) { + applyTo(target); + } + return verify; + } + + protected abstract void applyTo(List target) throws PatchFailedException; + + protected abstract void restore(List target); + + /** + * Apply patch fuzzy. + * + * @param target the list this patch will be applied to + * @param fuzz the number of elements to ignore before/after the patched elements + * @param position the position this patch will be applied to. ignores {@code source.getPosition()} + * @see Description of Fuzzy Patch for more information. + */ + @SuppressWarnings("RedundantThrows") + protected void applyFuzzyToAt(List target, int fuzz, int position) throws PatchFailedException { + throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not supports applying patch fuzzy"); + } + + /** + * Create a new delta of the actual instance with customized chunk data. + */ + public abstract AbstractDelta withChunks(Chunk original, Chunk revised); + + @Override + public int hashCode() { + return Objects.hash(this.source, this.target, this.type); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final AbstractDelta other = (AbstractDelta) obj; + if (!Objects.equals(this.source, other.source)) { + return false; + } + if (!Objects.equals(this.target, other.target)) { + return false; + } + return this.type == other.type; + } +} diff --git a/src/com/github/difflib/patch/ChangeDelta.java b/src/com/github/difflib/patch/ChangeDelta.java new file mode 100644 index 00000000..376fd625 --- /dev/null +++ b/src/com/github/difflib/patch/ChangeDelta.java @@ -0,0 +1,92 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +import java.util.List; +import java.util.Objects; + +/** + * Describes the change-delta between original and revised texts. + * + * @author Dmitry Naumenko + * @param The type of the compared elements in the data 'lines'. + */ +public final class ChangeDelta extends AbstractDelta { + + /** + * Creates a change delta with the two given chunks. + * + * @param source The source chunk. Must not be {@code null}. + * @param target The target chunk. Must not be {@code null}. + */ + public ChangeDelta(Chunk source, Chunk target) { + super(DeltaType.CHANGE, source, target); + Objects.requireNonNull(source, "source must not be null"); + Objects.requireNonNull(target, "target must not be null"); + } + + @Override + protected void applyTo(List target) throws PatchFailedException { + int position = getSource().getPosition(); + int size = getSource().size(); + for (int i = 0; i < size; i++) { + target.remove(position); + } + int i = 0; + for (T line : getTarget().getLines()) { + target.add(position + i, line); + i++; + } + } + + @Override + protected void restore(List target) { + int position = getTarget().getPosition(); + int size = getTarget().size(); + for (int i = 0; i < size; i++) { + target.remove(position); + } + int i = 0; + for (T line : getSource().getLines()) { + target.add(position + i, line); + i++; + } + } + + protected void applyFuzzyToAt(List target, int fuzz, int position) throws PatchFailedException { + int size = getSource().size(); + for (int i = fuzz; i < size - fuzz; i++) { + target.remove(position + fuzz); + } + + int i = fuzz; + for (T line : getTarget().getLines().subList(fuzz, getTarget().size() - fuzz)) { + target.add(position + i, line); + i++; + } + } + + @Override + public String toString() { + return "[ChangeDelta, position: " + getSource().getPosition() + ", lines: " + + getSource().getLines() + " to " + getTarget().getLines() + "]"; + } + + @Override + public AbstractDelta withChunks(Chunk original, Chunk revised) { + return new ChangeDelta(original, revised); + } +} diff --git a/src/com/github/difflib/patch/Chunk.java b/src/com/github/difflib/patch/Chunk.java new file mode 100644 index 00000000..072198a6 --- /dev/null +++ b/src/com/github/difflib/patch/Chunk.java @@ -0,0 +1,195 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Holds the information about the part of text involved in the diff process + * + *

+ * Text is represented as Object[] because the diff engine is + * capable of handling more than plain ascci. In fact, arrays or lists of any + * type that implements {@link Object#hashCode hashCode()} and + * {@link Object#equals equals()} correctly can be subject to + * differencing using this library. + *

+ * + * @author extends Serializable { + + public void processConflict(VerifyChunk verifyChunk, AbstractDelta delta, List result) throws PatchFailedException; +} diff --git a/src/com/github/difflib/patch/DeleteDelta.java b/src/com/github/difflib/patch/DeleteDelta.java new file mode 100644 index 00000000..890b8575 --- /dev/null +++ b/src/com/github/difflib/patch/DeleteDelta.java @@ -0,0 +1,66 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +import java.util.List; + +/** + * Describes the delete-delta between original and revised texts. + * + * @author Dmitry Naumenko + * @param The type of the compared elements in the 'lines'. + */ +public final class DeleteDelta extends AbstractDelta { + + /** + * Creates a change delta with the two given chunks. + * + * @param original The original chunk. Must not be {@code null}. + * @param revised The original chunk. Must not be {@code null}. + */ + public DeleteDelta(Chunk original, Chunk revised) { + super(DeltaType.DELETE, original, revised); + } + + @Override + protected void applyTo(List target) throws PatchFailedException { + int position = getSource().getPosition(); + int size = getSource().size(); + for (int i = 0; i < size; i++) { + target.remove(position); + } + } + + @Override + protected void restore(List target) { + int position = this.getTarget().getPosition(); + List lines = this.getSource().getLines(); + for (int i = 0; i < lines.size(); i++) { + target.add(position + i, lines.get(i)); + } + } + + @Override + public String toString() { + return "[DeleteDelta, position: " + getSource().getPosition() + ", lines: " + + getSource().getLines() + "]"; + } + + @Override + public AbstractDelta withChunks(Chunk original, Chunk revised) { + return new DeleteDelta(original, revised); + } +} diff --git a/src/com/github/difflib/patch/DeltaType.java b/src/com/github/difflib/patch/DeltaType.java new file mode 100644 index 00000000..666e803a --- /dev/null +++ b/src/com/github/difflib/patch/DeltaType.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +/** + * Specifies the type of the delta. There are three types of modifications from + * the original to get the revised text. + * + * CHANGE: a block of data of the original is replaced by another block of data. + * DELETE: a block of data of the original is removed + * INSERT: at a position of the original a block of data is inserted + * + * to be complete there is also + * + * EQUAL: a block of data of original and the revised text is equal + * + * which is no change at all. + * + */ +public enum DeltaType { + /** + * A change in the original. + */ + CHANGE, + /** + * A delete from the original. + */ + DELETE, + /** + * An insert into the original. + */ + INSERT, + /** + * An do nothing. + */ + EQUAL +} diff --git a/src/com/github/difflib/patch/DiffException.java b/src/com/github/difflib/patch/DiffException.java new file mode 100644 index 00000000..da01d621 --- /dev/null +++ b/src/com/github/difflib/patch/DiffException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +/** + * Base class for all exceptions emanating from this package. + * + * @author Juanco Anez + */ +public class DiffException extends Exception { + + private static final long serialVersionUID = 1L; + + public DiffException() { + } + + public DiffException(String msg) { + super(msg); + } +} diff --git a/src/com/github/difflib/patch/EqualDelta.java b/src/com/github/difflib/patch/EqualDelta.java new file mode 100644 index 00000000..17fdadc6 --- /dev/null +++ b/src/com/github/difflib/patch/EqualDelta.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +import java.util.List; + +/** + * This delta contains equal lines of data. Therefore nothing is to do in applyTo and restore. + * @author tobens + */ +public class EqualDelta extends AbstractDelta { + + public EqualDelta(Chunk source, Chunk target) { + super(DeltaType.EQUAL, source, target); + } + + @Override + protected void applyTo(List target) throws PatchFailedException { + } + + @Override + protected void restore(List target) { + } + + /** + * {@inheritDoc} + */ + @Override + protected void applyFuzzyToAt(List target, int fuzz, int delta) { + // equals so no operations + } + + @Override + public String toString() { + return "[EqualDelta, position: " + getSource().getPosition() + ", lines: " + + getSource().getLines() + "]"; + } + + @Override + public AbstractDelta withChunks(Chunk original, Chunk revised) { + return new EqualDelta(original, revised); + } +} diff --git a/src/com/github/difflib/patch/InsertDelta.java b/src/com/github/difflib/patch/InsertDelta.java new file mode 100644 index 00000000..6cff9103 --- /dev/null +++ b/src/com/github/difflib/patch/InsertDelta.java @@ -0,0 +1,66 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +import java.util.List; + +/** + * Describes the add-delta between original and revised texts. + * + * @author Dmitry Naumenko + * @param The type of the compared elements in the 'lines'. + */ +public final class InsertDelta extends AbstractDelta { + + /** + * Creates an insert delta with the two given chunks. + * + * @param original The original chunk. Must not be {@code null}. + * @param revised The original chunk. Must not be {@code null}. + */ + public InsertDelta(Chunk original, Chunk revised) { + super(DeltaType.INSERT, original, revised); + } + + @Override + protected void applyTo(List target) throws PatchFailedException { + int position = this.getSource().getPosition(); + List lines = this.getTarget().getLines(); + for (int i = 0; i < lines.size(); i++) { + target.add(position + i, lines.get(i)); + } + } + + @Override + protected void restore(List target) { + int position = getTarget().getPosition(); + int size = getTarget().size(); + for (int i = 0; i < size; i++) { + target.remove(position); + } + } + + @Override + public String toString() { + return "[InsertDelta, position: " + getSource().getPosition() + + ", lines: " + getTarget().getLines() + "]"; + } + + @Override + public AbstractDelta withChunks(Chunk original, Chunk revised) { + return new InsertDelta(original, revised); + } +} diff --git a/src/com/github/difflib/patch/Patch.java b/src/com/github/difflib/patch/Patch.java new file mode 100644 index 00000000..aaff7d94 --- /dev/null +++ b/src/com/github/difflib/patch/Patch.java @@ -0,0 +1,344 @@ +/*- + * #%L + * java-diff-utils + * %% + * Copyright (C) 2009 - 2017 java-diff-utils + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + * #L% + */ +package com.github.difflib.patch; + +import static java.util.Comparator.comparing; +import com.github.difflib.algorithm.Change; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; + +/** + * Describes the patch holding all deltas between the original and revised + * texts. + * + * @author Dmitry Naumenko + * @param The type of the compared elements in the 'lines'. + */ +public final class Patch implements Serializable { + + private final List> deltas; + + public Patch() { + this(10); + } + + public Patch(int estimatedPatchSize) { + deltas = new ArrayList<>(estimatedPatchSize); + } + + /** + * Creates a new list, the patch is being applied to. + * + * @param target The list to apply the changes to. + * @return A new list containing the applied patch. + * @throws PatchFailedException if the patch cannot be applied + */ + public List applyTo(List target) throws PatchFailedException { + List result = new ArrayList<>(target); + applyToExisting(result); + return result; + } + + /** + * Applies the patch to the supplied list. + * + * @param target The list to apply the changes to. This list has to be modifiable, + * otherwise exceptions may be thrown, depending on the used type of list. + * @throws PatchFailedException if the patch cannot be applied + * @throws RuntimeException (or similar) if the list is not modifiable. + */ + public void applyToExisting(List target) throws PatchFailedException { + ListIterator> it = getDeltas().listIterator(deltas.size()); + while (it.hasPrevious()) { + AbstractDelta delta = it.previous(); + VerifyChunk valid = delta.verifyAndApplyTo(target); + if (valid != VerifyChunk.OK) { + conflictOutput.processConflict(valid, delta, target); + } + } + } + + private static class PatchApplyingContext { + public final List result; + public final int maxFuzz; + + // the position last patch applied to. + public int lastPatchEnd = -1; + + ///// passing values from find to apply + public int currentFuzz = 0; + + public int defaultPosition; + public boolean beforeOutRange = false; + public boolean afterOutRange = false; + + private PatchApplyingContext(List result, int maxFuzz) { + this.result = result; + this.maxFuzz = maxFuzz; + } + } + + public List applyFuzzy(List target, int maxFuzz) throws PatchFailedException { + PatchApplyingContext ctx = new PatchApplyingContext<>(new ArrayList<>(target), maxFuzz); + + // the difference between patch's position and actually applied position + int lastPatchDelta = 0; + + for (AbstractDelta delta : getDeltas()) { + ctx.defaultPosition = delta.getSource().getPosition() + lastPatchDelta; + int patchPosition = findPositionFuzzy(ctx, delta); + if (0 <= patchPosition) { + delta.applyFuzzyToAt(ctx.result, ctx.currentFuzz, patchPosition); + lastPatchDelta = patchPosition - delta.getSource().getPosition(); + ctx.lastPatchEnd = delta.getSource().last() + lastPatchDelta; + } else { + conflictOutput.processConflict(VerifyChunk.CONTENT_DOES_NOT_MATCH_TARGET, delta, ctx.result); + } + } + + return ctx.result; + } + + // negative for not found + private int findPositionFuzzy(PatchApplyingContext ctx, AbstractDelta delta) throws PatchFailedException { + for (int fuzz = 0; fuzz <= ctx.maxFuzz; fuzz++) { + ctx.currentFuzz = fuzz; + int foundPosition = findPositionWithFuzz(ctx, delta, fuzz); + if (foundPosition >= 0) { + return foundPosition; + } + } + return -1; + } + + // negative for not found + private int findPositionWithFuzz(PatchApplyingContext ctx, AbstractDelta delta, int fuzz) throws PatchFailedException { + if (delta.getSource().verifyChunk(ctx.result, fuzz, ctx.defaultPosition) == VerifyChunk.OK) { + return ctx.defaultPosition; + } + + ctx.beforeOutRange = false; + ctx.afterOutRange = false; + + // moreDelta >= 0: just for overflow guard, not a normal condition + //noinspection OverflowingLoopIndex + for (int moreDelta = 0; moreDelta >= 0; moreDelta++) { + int pos = findPositionWithFuzzAndMoreDelta(ctx, delta, fuzz, moreDelta); + if (pos >= 0) { + return pos; + } + if (ctx.beforeOutRange && ctx.afterOutRange) { + break; + } + } + + return -1; + } + + // negative for not found + private int findPositionWithFuzzAndMoreDelta(PatchApplyingContext ctx, AbstractDelta delta, int fuzz, int moreDelta) throws PatchFailedException { + // range check: can't apply before end of last patch + if (!ctx.beforeOutRange) { + int beginAt = ctx.defaultPosition - moreDelta + fuzz; + // We can't apply patch before end of last patch. + if (beginAt <= ctx.lastPatchEnd) { + ctx.beforeOutRange = true; + } + } + // range check: can't apply after end of result + if (!ctx.afterOutRange) { + int beginAt = ctx.defaultPosition + moreDelta + delta.getSource().size() - fuzz; + // We can't apply patch before end of last patch. + if (ctx.result.size() < beginAt) { + ctx.afterOutRange = true; + } + } + + if (!ctx.beforeOutRange) { + VerifyChunk before = delta.getSource().verifyChunk(ctx.result, fuzz, ctx.defaultPosition - moreDelta); + if (before == VerifyChunk.OK) { + return ctx.defaultPosition - moreDelta; + } + } + if (!ctx.afterOutRange) { + VerifyChunk after = delta.getSource().verifyChunk(ctx.result, fuzz, ctx.defaultPosition + moreDelta); + if (after == VerifyChunk.OK) { + return ctx.defaultPosition + moreDelta; + } + } + return -1; + } + + /** + * Standard Patch behaviour to throw an exception for pathching conflicts. + */ + public final ConflictOutput CONFLICT_PRODUCES_EXCEPTION = (VerifyChunk verifyChunk, AbstractDelta delta, List result) -> { + throw new PatchFailedException("could not apply patch due to " + verifyChunk.toString()); + }; + + /** + * Git like merge conflict output. + */ + public static final ConflictOutput CONFLICT_PRODUCES_MERGE_CONFLICT = (VerifyChunk verifyChunk, AbstractDelta delta, List result) -> { + if (result.size() > delta.getSource().getPosition()) { + List orgData = new ArrayList<>(); + + for (int i = 0; i < delta.getSource().size(); i++) { + orgData.add(result.get(delta.getSource().getPosition())); + result.remove(delta.getSource().getPosition()); + } + + orgData.add(0, "<<<<<< HEAD"); + orgData.add("======"); + orgData.addAll(delta.getSource().getLines()); + orgData.add(">>>>>>> PATCH"); + + result.addAll(delta.getSource().getPosition(), orgData); + + } else { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + }; + + private ConflictOutput conflictOutput = CONFLICT_PRODUCES_EXCEPTION; + + /** + * Alter normal conflict output behaviour to e.g. inclide some conflict + * statements in the result, like git does it. + */ + public Patch withConflictOutput(ConflictOutput conflictOutput) { + this.conflictOutput = conflictOutput; + return this; + } + + /** + * Creates a new list, containing the restored state of the given list. + * Opposite to {@link #applyTo(List)} method. + * + * @param target The list to copy and apply changes to. + * @return A new list, containing the restored state. + */ + public List restore(List target) { + List result = new ArrayList<>(target); + restoreToExisting(result); + return result; + } + + + /** + * Restores all changes within the given list. + * Opposite to {@link #applyToExisting(List)} method. + * + * @param target The list to restore changes in. This list has to be modifiable, + * otherwise exceptions may be thrown, depending on the used type of list. + * @throws RuntimeException (or similar) if the list is not modifiable. + */ + public void restoreToExisting(List target) { + ListIterator> it = getDeltas().listIterator(deltas.size()); + while (it.hasPrevious()) { + AbstractDelta delta = it.previous(); + delta.restore(target); + } + } + + /** + * Add the given delta to this patch + * + * @param delta the given delta + */ + public void addDelta(AbstractDelta delta) { + deltas.add(delta); + } + + /** + * Get the list of computed deltas + * + * @return the deltas + */ + public List> getDeltas() { + deltas.sort(comparing(d -> d.getSource().getPosition())); + return deltas; + } + + @Override + public String toString() { + return "Patch{" + "deltas=" + deltas + '}'; + } + + public static Patch generate(List original, List revised, List changes) { + return generate(original, revised, changes, false); + } + + private static Chunk buildChunk(int start, int end, List data) { + return new Chunk<>(start, new ArrayList<>(data.subList(start, end))); + } + + public static Patch generate(List original, List revised, List _changes, boolean includeEquals) { + Patch patch = new Patch<>(_changes.size()); + int startOriginal = 0; + int startRevised = 0; + + List changes = _changes; + + if (includeEquals) { + changes = new ArrayList(_changes); + Collections.sort(changes, comparing(d -> d.startOriginal)); + } + + for (Change change : changes) { + + if (includeEquals && startOriginal < change.startOriginal) { + patch.addDelta(new EqualDelta( + buildChunk(startOriginal, change.startOriginal, original), + buildChunk(startRevised, change.startRevised, revised))); + } + + Chunk orgChunk = buildChunk(change.startOriginal, change.endOriginal, original); + Chunk revChunk = buildChunk(change.startRevised, change.endRevised, revised); + switch (change.deltaType) { + case DELETE: + patch.addDelta(new DeleteDelta<>(orgChunk, revChunk)); + break; + case INSERT: + patch.addDelta(new InsertDelta<>(orgChunk, revChunk)); + break; + case CHANGE: + patch.addDelta(new ChangeDelta<>(orgChunk, revChunk)); + break; + default: + } + + startOriginal = change.endOriginal; + startRevised = change.endRevised; + } + + if (includeEquals && startOriginal < original.size()) { + patch.addDelta(new EqualDelta( + buildChunk(startOriginal, original.size(), original), + buildChunk(startRevised, revised.size(), revised))); + } + + return patch; + } +} diff --git a/src/com/github/difflib/patch/PatchFailedException.java b/src/com/github/difflib/patch/PatchFailedException.java new file mode 100644 index 00000000..7521c892 --- /dev/null +++ b/src/com/github/difflib/patch/PatchFailedException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +/** + * Thrown whenever a delta cannot be applied as a patch to a given text. + * + * @author Juanco Anez + */ +public class PatchFailedException extends DiffException { + + private static final long serialVersionUID = 1L; + + public PatchFailedException() { + } + + public PatchFailedException(String msg) { + super(msg); + } +} diff --git a/src/com/github/difflib/patch/VerifyChunk.java b/src/com/github/difflib/patch/VerifyChunk.java new file mode 100644 index 00000000..076f633a --- /dev/null +++ b/src/com/github/difflib/patch/VerifyChunk.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.patch; + +/** + * + * @author tw + */ +public enum VerifyChunk { + OK, + POSITION_OUT_OF_TARGET, + CONTENT_DOES_NOT_MATCH_TARGET +} diff --git a/src/com/github/difflib/text/DiffRow.java b/src/com/github/difflib/text/DiffRow.java new file mode 100644 index 00000000..95908393 --- /dev/null +++ b/src/com/github/difflib/text/DiffRow.java @@ -0,0 +1,115 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.text; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Describes the diff row in form [tag, oldLine, newLine) for showing the difference between two texts + * + * @author Dmitry Naumenko + */ +public final class DiffRow implements Serializable { + + private Tag tag; + private final String oldLine; + private final String newLine; + + public DiffRow(Tag tag, String oldLine, String newLine) { + this.tag = tag; + this.oldLine = oldLine; + this.newLine = newLine; + } + + public enum Tag { + INSERT, DELETE, CHANGE, EQUAL + } + + /** + * @return the tag + */ + public Tag getTag() { + return tag; + } + + /** + * @param tag the tag to set + */ + public void setTag(Tag tag) { + this.tag = tag; + } + + /** + * @return the oldLine + */ + public String getOldLine() { + return oldLine; + } + + /** + * @return the newLine + */ + public String getNewLine() { + return newLine; + } + + @Override + public int hashCode() { + return Objects.hash(newLine, oldLine, tag); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DiffRow other = (DiffRow) obj; + if (newLine == null) { + if (other.newLine != null) { + return false; + } + } else if (!newLine.equals(other.newLine)) { + return false; + } + if (oldLine == null) { + if (other.oldLine != null) { + return false; + } + } else if (!oldLine.equals(other.oldLine)) { + return false; + } + if (tag == null) { + if (other.tag != null) { + return false; + } + } else if (!tag.equals(other.tag)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "[" + this.tag + "," + this.oldLine + "," + this.newLine + "]"; + } +} diff --git a/src/com/github/difflib/text/DiffRowGenerator.java b/src/com/github/difflib/text/DiffRowGenerator.java new file mode 100644 index 00000000..9ec50a84 --- /dev/null +++ b/src/com/github/difflib/text/DiffRowGenerator.java @@ -0,0 +1,706 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.text; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.ChangeDelta; +import com.github.difflib.patch.Chunk; +import com.github.difflib.patch.DeleteDelta; +import com.github.difflib.patch.DeltaType; +import com.github.difflib.patch.InsertDelta; +import com.github.difflib.patch.Patch; +import com.github.difflib.text.DiffRow.Tag; +import com.github.difflib.text.deltamerge.DeltaMergeUtils; +import com.github.difflib.text.deltamerge.InlineDeltaMergeInfo; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static java.util.stream.Collectors.toList; + +/** + * This class for generating DiffRows for side-by-sidy view. You can customize + * the way of generating. For example, show inline diffs on not, ignoring white + * spaces or/and blank lines and so on. All parameters for generating are + * optional. If you do not specify them, the class will use the default values. + * + * These values are: showInlineDiffs = false; ignoreWhiteSpaces = true; + * ignoreBlankLines = true; ... + * + * For instantiating the DiffRowGenerator you should use the its builder. Like + * in example + * DiffRowGenerator generator = new DiffRowGenerator.Builder().showInlineDiffs(true). + * ignoreWhiteSpaces(true).columnWidth(100).build(); + * + */ +public final class DiffRowGenerator { + + public static final BiPredicate DEFAULT_EQUALIZER = Object::equals; + + public static final BiPredicate IGNORE_WHITESPACE_EQUALIZER = (original, revised) + -> adjustWhitespace(original).equals(adjustWhitespace(revised)); + + public static final Function LINE_NORMALIZER_FOR_HTML = StringUtils::normalize; + + /** + * Splitting lines by character to achieve char by char diff checking. + */ + public static final Function> SPLITTER_BY_CHARACTER = line -> { + List list = new ArrayList<>(line.length()); + for (Character character : line.toCharArray()) { + list.add(character.toString()); + } + return list; + }; + + public static final Pattern SPLIT_BY_WORD_PATTERN = Pattern.compile("\\s+|[,.\\[\\](){}/\\\\*+\\-#<>;:&\\']+"); + + /** + * Splitting lines by word to achieve word by word diff checking. + */ + public static final Function> SPLITTER_BY_WORD = line -> splitStringPreserveDelimiter(line, SPLIT_BY_WORD_PATTERN); + public static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + + public static final Function>> DEFAULT_INLINE_DELTA_MERGER = InlineDeltaMergeInfo::getDeltas; + + /** + * Merge diffs which are separated by equalities consisting of whitespace only. + */ + public static final Function>> WHITESPACE_EQUALITIES_MERGER = deltaMergeInfo -> DeltaMergeUtils + .mergeInlineDeltas(deltaMergeInfo, equalities -> equalities.stream().allMatch(s -> s==null || s.replaceAll("\\s+", "").equals(""))); + + public static Builder create() { + return new Builder(); + } + + private static String adjustWhitespace(String raw) { + return WHITESPACE_PATTERN.matcher(raw.trim()).replaceAll(" "); + } + + protected final static List splitStringPreserveDelimiter(String str, Pattern SPLIT_PATTERN) { + List list = new ArrayList<>(); + if (str != null) { + Matcher matcher = SPLIT_PATTERN.matcher(str); + int pos = 0; + while (matcher.find()) { + if (pos < matcher.start()) { + list.add(str.substring(pos, matcher.start())); + } + list.add(matcher.group()); + pos = matcher.end(); + } + if (pos < str.length()) { + list.add(str.substring(pos)); + } + } + return list; + } + + /** + * Wrap the elements in the sequence with the given tag + * + * @param startPosition the position from which tag should start. The + * counting start from a zero. + * @param endPosition the position before which tag should should be closed. + * @param tagGenerator the tag generator + */ + static void wrapInTag(List sequence, int startPosition, + int endPosition, Tag tag, BiFunction tagGenerator, + Function processDiffs, boolean replaceLinefeedWithSpace) { + int endPos = endPosition; + + while (endPos >= startPosition) { + + //search position for end tag + while (endPos > startPosition) { + if (!"\n".equals(sequence.get(endPos - 1))) { + break; + } else if (replaceLinefeedWithSpace) { + sequence.set(endPos - 1, " "); + break; + } + endPos--; + } + + if (endPos == startPosition) { + break; + } + + sequence.add(endPos, tagGenerator.apply(tag, false)); + if (processDiffs != null) { + sequence.set(endPos - 1, + processDiffs.apply(sequence.get(endPos - 1))); + } + endPos--; + + //search position for end tag + while (endPos > startPosition) { + if ("\n".equals(sequence.get(endPos - 1))) { + if (replaceLinefeedWithSpace) { + sequence.set(endPos - 1, " "); + } else { + break; + } + } + if (processDiffs != null) { + sequence.set(endPos - 1, + processDiffs.apply(sequence.get(endPos - 1))); + } + endPos--; + } + + sequence.add(endPos, tagGenerator.apply(tag, true)); + endPos--; + } + } + + private final int columnWidth; + private final BiPredicate equalizer; + private final boolean ignoreWhiteSpaces; + private final Function> inlineDiffSplitter; + private final boolean mergeOriginalRevised; + private final BiFunction newTag; + private final BiFunction oldTag; + private final boolean reportLinesUnchanged; + private final Function lineNormalizer; + private final Function processDiffs; + private final Function>> inlineDeltaMerger; + + private final boolean showInlineDiffs; + private final boolean replaceOriginalLinefeedInChangesWithSpaces; + private final boolean decompressDeltas; + + private DiffRowGenerator(Builder builder) { + showInlineDiffs = builder.showInlineDiffs; + ignoreWhiteSpaces = builder.ignoreWhiteSpaces; + oldTag = builder.oldTag; + newTag = builder.newTag; + columnWidth = builder.columnWidth; + mergeOriginalRevised = builder.mergeOriginalRevised; + inlineDiffSplitter = builder.inlineDiffSplitter; + decompressDeltas = builder.decompressDeltas; + + if (builder.equalizer != null) { + equalizer = builder.equalizer; + } else { + equalizer = ignoreWhiteSpaces ? IGNORE_WHITESPACE_EQUALIZER : DEFAULT_EQUALIZER; + } + + reportLinesUnchanged = builder.reportLinesUnchanged; + lineNormalizer = builder.lineNormalizer; + processDiffs = builder.processDiffs; + inlineDeltaMerger = builder.inlineDeltaMerger; + + replaceOriginalLinefeedInChangesWithSpaces = builder.replaceOriginalLinefeedInChangesWithSpaces; + + Objects.requireNonNull(inlineDiffSplitter); + Objects.requireNonNull(lineNormalizer); + Objects.requireNonNull(inlineDeltaMerger); + } + + /** + * Get the DiffRows describing the difference between original and revised + * texts using the given patch. Useful for displaying side-by-side diff. + * + * @param original the original text + * @param revised the revised text + * @return the DiffRows between original and revised texts + */ + public List generateDiffRows(List original, List revised) { + return generateDiffRows(original, DiffUtils.diff(original, revised, equalizer)); + } + + /** + * Generates the DiffRows describing the difference between original and + * revised texts using the given patch. Useful for displaying side-by-side + * diff. + * + * @param original the original text + * @param patch the given patch + * @return the DiffRows between original and revised texts + */ + public List generateDiffRows(final List original, Patch patch) { + List diffRows = new ArrayList<>(); + int endPos = 0; + final List> deltaList = patch.getDeltas(); + + if (decompressDeltas) { + for (AbstractDelta originalDelta : deltaList) { + for (AbstractDelta delta : decompressDeltas(originalDelta)) { + endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta); + } + } + } else { + for (AbstractDelta delta : deltaList) { + endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta); + } + } + + // Copy the final matching chunk if any. + for (String line : original.subList(endPos, original.size())) { + diffRows.add(buildDiffRow(Tag.EQUAL, line, line)); + } + return diffRows; + } + + /** + * Transforms one patch delta into a DiffRow object. + */ + private int transformDeltaIntoDiffRow(final List original, int endPos, List diffRows, AbstractDelta delta) { + Chunk orig = delta.getSource(); + Chunk rev = delta.getTarget(); + + for (String line : original.subList(endPos, orig.getPosition())) { + diffRows.add(buildDiffRow(Tag.EQUAL, line, line)); + } + + switch (delta.getType()) { + case INSERT: + for (String line : rev.getLines()) { + diffRows.add(buildDiffRow(Tag.INSERT, "", line)); + } + break; + case DELETE: + for (String line : orig.getLines()) { + diffRows.add(buildDiffRow(Tag.DELETE, line, "")); + } + break; + default: + if (showInlineDiffs) { + diffRows.addAll(generateInlineDiffs(delta)); + } else { + for (int j = 0; j < Math.max(orig.size(), rev.size()); j++) { + diffRows.add(buildDiffRow(Tag.CHANGE, + orig.getLines().size() > j ? orig.getLines().get(j) : "", + rev.getLines().size() > j ? rev.getLines().get(j) : "")); + } + } + } + + return orig.last() + 1; + } + + /** + * Decompresses ChangeDeltas with different source and target size to a + * ChangeDelta with same size and a following InsertDelta or DeleteDelta. + * With this problems of building DiffRows getting smaller. + * + * @param deltaList + */ + private List> decompressDeltas(AbstractDelta delta) { + if (delta.getType() == DeltaType.CHANGE && delta.getSource().size() != delta.getTarget().size()) { + List> deltas = new ArrayList<>(); + //System.out.println("decompress this " + delta); + + int minSize = Math.min(delta.getSource().size(), delta.getTarget().size()); + Chunk orig = delta.getSource(); + Chunk rev = delta.getTarget(); + + deltas.add(new ChangeDelta( + new Chunk<>(orig.getPosition(), orig.getLines().subList(0, minSize)), + new Chunk<>(rev.getPosition(), rev.getLines().subList(0, minSize)))); + + if (orig.getLines().size() < rev.getLines().size()) { + deltas.add(new InsertDelta( + new Chunk<>(orig.getPosition() + minSize, Collections.emptyList()), + new Chunk<>(rev.getPosition() + minSize, rev.getLines().subList(minSize, rev.getLines().size())))); + } else { + deltas.add(new DeleteDelta( + new Chunk<>(orig.getPosition() + minSize, orig.getLines().subList(minSize, orig.getLines().size())), + new Chunk<>(rev.getPosition() + minSize, Collections.emptyList()))); + } + return deltas; + } + + return Collections.singletonList(delta); + } + + private DiffRow buildDiffRow(Tag type, String orgline, String newline) { + if (reportLinesUnchanged) { + return new DiffRow(type, orgline, newline); + } else { + String wrapOrg = preprocessLine(orgline); + if (Tag.DELETE == type) { + if (mergeOriginalRevised || showInlineDiffs) { + wrapOrg = oldTag.apply(type, true) + wrapOrg + oldTag.apply(type, false); + } + } + String wrapNew = preprocessLine(newline); + if (Tag.INSERT == type) { + if (mergeOriginalRevised) { + wrapOrg = newTag.apply(type, true) + wrapNew + newTag.apply(type, false); + } else if (showInlineDiffs) { + wrapNew = newTag.apply(type, true) + wrapNew + newTag.apply(type, false); + } + } + return new DiffRow(type, wrapOrg, wrapNew); + } + } + + private DiffRow buildDiffRowWithoutNormalizing(Tag type, String orgline, String newline) { + return new DiffRow(type, + StringUtils.wrapText(orgline, columnWidth), + StringUtils.wrapText(newline, columnWidth)); + } + + List normalizeLines(List list) { + return reportLinesUnchanged + ? list + : list.stream() + .map(lineNormalizer::apply) + .collect(toList()); + } + + /** + * Add the inline diffs for given delta + * + * @param delta the given delta + */ + private List generateInlineDiffs(AbstractDelta delta) { + List orig = normalizeLines(delta.getSource().getLines()); + List rev = normalizeLines(delta.getTarget().getLines()); + List origList; + List revList; + String joinedOrig = String.join("\n", orig); + String joinedRev = String.join("\n", rev); + + origList = inlineDiffSplitter.apply(joinedOrig); + revList = inlineDiffSplitter.apply(joinedRev); + + List> originalInlineDeltas = DiffUtils.diff(origList, revList, equalizer) + .getDeltas(); + List> inlineDeltas = inlineDeltaMerger + .apply(new InlineDeltaMergeInfo(originalInlineDeltas, origList, revList)); + + Collections.reverse(inlineDeltas); + for (AbstractDelta inlineDelta : inlineDeltas) { + Chunk inlineOrig = inlineDelta.getSource(); + Chunk inlineRev = inlineDelta.getTarget(); + if (inlineDelta.getType() == DeltaType.DELETE) { + wrapInTag(origList, inlineOrig.getPosition(), inlineOrig + .getPosition() + + inlineOrig.size(), Tag.DELETE, oldTag, processDiffs, replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised); + } else if (inlineDelta.getType() == DeltaType.INSERT) { + if (mergeOriginalRevised) { + origList.addAll(inlineOrig.getPosition(), + revList.subList(inlineRev.getPosition(), + inlineRev.getPosition() + inlineRev.size())); + wrapInTag(origList, inlineOrig.getPosition(), + inlineOrig.getPosition() + inlineRev.size(), + Tag.INSERT, newTag, processDiffs, false); + } else { + wrapInTag(revList, inlineRev.getPosition(), + inlineRev.getPosition() + inlineRev.size(), + Tag.INSERT, newTag, processDiffs, false); + } + } else if (inlineDelta.getType() == DeltaType.CHANGE) { + if (mergeOriginalRevised) { + origList.addAll(inlineOrig.getPosition() + inlineOrig.size(), + revList.subList(inlineRev.getPosition(), + inlineRev.getPosition() + inlineRev.size())); + wrapInTag(origList, inlineOrig.getPosition() + inlineOrig.size(), + inlineOrig.getPosition() + inlineOrig.size() + inlineRev.size(), + Tag.CHANGE, newTag, processDiffs, false); + } else { + wrapInTag(revList, inlineRev.getPosition(), + inlineRev.getPosition() + inlineRev.size(), + Tag.CHANGE, newTag, processDiffs, false); + } + wrapInTag(origList, inlineOrig.getPosition(), + inlineOrig.getPosition() + inlineOrig.size(), + Tag.CHANGE, oldTag, processDiffs, replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised); + } + } + StringBuilder origResult = new StringBuilder(); + StringBuilder revResult = new StringBuilder(); + for (String character : origList) { + origResult.append(character); + } + for (String character : revList) { + revResult.append(character); + } + + List original = Arrays.asList(origResult.toString().split("\n")); + List revised = Arrays.asList(revResult.toString().split("\n")); + List diffRows = new ArrayList<>(); + for (int j = 0; j < Math.max(original.size(), revised.size()); j++) { + diffRows. + add(buildDiffRowWithoutNormalizing(Tag.CHANGE, + original.size() > j ? original.get(j) : "", + revised.size() > j ? revised.get(j) : "")); + } + return diffRows; + } + + private String preprocessLine(String line) { + if (columnWidth == 0) { + return lineNormalizer.apply(line); + } else { + return StringUtils.wrapText(lineNormalizer.apply(line), columnWidth); + } + } + + /** + * This class used for building the DiffRowGenerator. + * + * @author dmitry + * + */ + public static class Builder { + + private boolean showInlineDiffs = false; + private boolean ignoreWhiteSpaces = false; + private boolean decompressDeltas = true; + + private BiFunction oldTag + = (tag, f) -> f ? "" : ""; + private BiFunction newTag + = (tag, f) -> f ? "" : ""; + + private int columnWidth = 0; + private boolean mergeOriginalRevised = false; + private boolean reportLinesUnchanged = false; + private Function> inlineDiffSplitter = SPLITTER_BY_CHARACTER; + private Function lineNormalizer = LINE_NORMALIZER_FOR_HTML; + private Function processDiffs = null; + private BiPredicate equalizer = null; + private boolean replaceOriginalLinefeedInChangesWithSpaces = false; + private Function>> inlineDeltaMerger = DEFAULT_INLINE_DELTA_MERGER; + + private Builder() { + } + + /** + * Show inline diffs in generating diff rows or not. + * + * @param val the value to set. Default: false. + * @return builder with configured showInlineDiff parameter + */ + public Builder showInlineDiffs(boolean val) { + showInlineDiffs = val; + return this; + } + + /** + * Ignore white spaces in generating diff rows or not. + * + * @param val the value to set. Default: true. + * @return builder with configured ignoreWhiteSpaces parameter + */ + public Builder ignoreWhiteSpaces(boolean val) { + ignoreWhiteSpaces = val; + return this; + } + + /** + * Report all lines without markup on the old or new text. + * + * @param val the value to set. Default: false. + * @return builder with configured reportLinesUnchanged parameter + */ + public Builder reportLinesUnchanged(final boolean val) { + reportLinesUnchanged = val; + return this; + } + + /** + * Generator for Old-Text-Tags. + * + * @param generator the tag generator + * @return builder with configured ignoreBlankLines parameter + */ + public Builder oldTag(BiFunction generator) { + this.oldTag = generator; + return this; + } + + /** + * Generator for Old-Text-Tags. + * + * @param generator the tag generator + * @return builder with configured ignoreBlankLines parameter + */ + public Builder oldTag(Function generator) { + this.oldTag = (tag, f) -> generator.apply(f); + return this; + } + + /** + * Generator for New-Text-Tags. + * + * @param generator + * @return + */ + public Builder newTag(BiFunction generator) { + this.newTag = generator; + return this; + } + + /** + * Generator for New-Text-Tags. + * + * @param generator + * @return + */ + public Builder newTag(Function generator) { + this.newTag = (tag, f) -> generator.apply(f); + return this; + } + + /** + * Processor for diffed text parts. Here e.g. whitecharacters could be + * replaced by something visible. + * + * @param processDiffs + * @return + */ + public Builder processDiffs(Function processDiffs) { + this.processDiffs = processDiffs; + return this; + } + + /** + * Set the column width of generated lines of original and revised + * texts. + * + * @param width the width to set. Making it < 0 doesn't make any + * sense. Default 80. + * @return builder with config of column width + */ + public Builder columnWidth(int width) { + if (width >= 0) { + columnWidth = width; + } + return this; + } + + /** + * Build the DiffRowGenerator. If some parameters is not set, the + * default values are used. + * + * @return the customized DiffRowGenerator + */ + public DiffRowGenerator build() { + return new DiffRowGenerator(this); + } + + /** + * Merge the complete result within the original text. This makes sense + * for one line display. + * + * @param mergeOriginalRevised + * @return + */ + public Builder mergeOriginalRevised(boolean mergeOriginalRevised) { + this.mergeOriginalRevised = mergeOriginalRevised; + return this; + } + + /** + * Deltas could be in a state, that would produce some unreasonable + * results within an inline diff. So the deltas are decompressed into + * smaller parts and rebuild. But this could result in more differences. + * + * @param decompressDeltas + * @return + */ + public Builder decompressDeltas(boolean decompressDeltas) { + this.decompressDeltas = decompressDeltas; + return this; + } + + /** + * Per default each character is separatly processed. This variant + * introduces processing by word, which does not deliver in word + * changes. Therefore the whole word will be tagged as changed: + * + *
+         * false:    (aBa : aba) --  changed: a(B)a : a(b)a
+         * true:     (aBa : aba) --  changed: (aBa) : (aba)
+         * 
+ */ + public Builder inlineDiffByWord(boolean inlineDiffByWord) { + inlineDiffSplitter = inlineDiffByWord ? SPLITTER_BY_WORD : SPLITTER_BY_CHARACTER; + return this; + } + + /** + * To provide some customized splitting a splitter can be provided. Here + * someone could think about sentence splitter, comma splitter or stuff + * like that. + * + * @param inlineDiffSplitter + * @return + */ + public Builder inlineDiffBySplitter(Function> inlineDiffSplitter) { + this.inlineDiffSplitter = inlineDiffSplitter; + return this; + } + + /** + * By default DiffRowGenerator preprocesses lines for HTML output. Tabs + * and special HTML characters like "<" are replaced with its encoded + * value. To change this you can provide a customized line normalizer + * here. + * + * @param lineNormalizer + * @return + */ + public Builder lineNormalizer(Function lineNormalizer) { + this.lineNormalizer = lineNormalizer; + return this; + } + + /** + * Provide an equalizer for diff processing. + * + * @param equalizer equalizer for diff processing. + * @return builder with configured equalizer parameter + */ + public Builder equalizer(BiPredicate equalizer) { + this.equalizer = equalizer; + return this; + } + + /** + * Sometimes it happens that a change contains multiple lines. If there + * is no correspondence in old and new. To keep the merged line more + * readable the linefeeds could be replaced by spaces. + * + * @param replace + * @return + */ + public Builder replaceOriginalLinefeedInChangesWithSpaces(boolean replace) { + this.replaceOriginalLinefeedInChangesWithSpaces = replace; + return this; + } + + /** + * Provide an inline delta merger for use case specific delta optimizations. + * + * @param inlineDeltaMerger + * @return + */ + public Builder inlineDeltaMerger( + Function>> inlineDeltaMerger) { + this.inlineDeltaMerger = inlineDeltaMerger; + return this; + } + } +} diff --git a/src/com/github/difflib/text/StringUtils.java b/src/com/github/difflib/text/StringUtils.java new file mode 100644 index 00000000..b7e35495 --- /dev/null +++ b/src/com/github/difflib/text/StringUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright 2009-2017 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.text; + +import java.util.List; +import static java.util.stream.Collectors.toList; + +final class StringUtils { + + /** + * Replaces all opening and closing tags with < or >. + * + * @param str + * @return str with some HTML meta characters escaped. + */ + public static String htmlEntites(String str) { + return str.replace("<", "<").replace(">", ">"); + } + + public static String normalize(String str) { + return htmlEntites(str).replace("\t", " "); + } + + public static List wrapText(List list, int columnWidth) { + return list.stream() + .map(line -> wrapText(line, columnWidth)) + .collect(toList()); + } + + /** + * Wrap the text with the given column width + * + * @param line the text + * @param columnWidth the given column + * @return the wrapped text + */ + public static String wrapText(String line, int columnWidth) { + if (columnWidth < 0) { + throw new IllegalArgumentException("columnWidth may not be less 0"); + } + if (columnWidth == 0) { + return line; + } + int length = line.length(); + int delimiter = "
".length(); + int widthIndex = columnWidth; + + StringBuilder b = new StringBuilder(line); + + for (int count = 0; length > widthIndex; count++) { + int breakPoint = widthIndex + delimiter * count; + if (Character.isHighSurrogate(b.charAt(breakPoint - 1)) && + Character.isLowSurrogate(b.charAt(breakPoint))) { + // Shift a breakpoint that would split a supplemental code-point. + breakPoint += 1; + if (breakPoint == b.length()) { + // Break before instead of after if this is the last code-point. + breakPoint -= 2; + } + } + b.insert(breakPoint, "
"); + widthIndex += columnWidth; + } + + return b.toString(); + } + + private StringUtils() { + } +} diff --git a/src/com/github/difflib/text/deltamerge/DeltaMergeUtils.java b/src/com/github/difflib/text/deltamerge/DeltaMergeUtils.java new file mode 100644 index 00000000..b2580957 --- /dev/null +++ b/src/com/github/difflib/text/deltamerge/DeltaMergeUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2009-2024 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.text.deltamerge; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.ChangeDelta; +import com.github.difflib.patch.Chunk; + +/** + * Provides utility features for merge inline deltas + * + * @author Christian Meier + */ +final public class DeltaMergeUtils { + + public static List> mergeInlineDeltas(InlineDeltaMergeInfo deltaMergeInfo, + Predicate> replaceEquality) { + final List> originalDeltas = deltaMergeInfo.getDeltas(); + if (originalDeltas.size() < 2) { + return originalDeltas; + } + + final List> newDeltas = new ArrayList<>(); + newDeltas.add(originalDeltas.get(0)); + for (int i = 1; i < originalDeltas.size(); i++) { + final AbstractDelta previousDelta = newDeltas.get(newDeltas.size()-1); + final AbstractDelta currentDelta = originalDeltas.get(i); + + final List equalities = deltaMergeInfo.getOrigList().subList( + previousDelta.getSource().getPosition() + previousDelta.getSource().size(), + currentDelta.getSource().getPosition()); + + if (replaceEquality.test(equalities)) { + // Merge the previous delta, the equality and the current delta into one + // ChangeDelta and replace the previous delta by this new ChangeDelta. + final List allSourceLines = new ArrayList<>(); + allSourceLines.addAll(previousDelta.getSource().getLines()); + allSourceLines.addAll(equalities); + allSourceLines.addAll(currentDelta.getSource().getLines()); + + final List allTargetLines = new ArrayList<>(); + allTargetLines.addAll(previousDelta.getTarget().getLines()); + allTargetLines.addAll(equalities); + allTargetLines.addAll(currentDelta.getTarget().getLines()); + + final ChangeDelta replacement = new ChangeDelta<>( + new Chunk<>(previousDelta.getSource().getPosition(), allSourceLines), + new Chunk<>(previousDelta.getTarget().getPosition(), allTargetLines)); + + newDeltas.remove(newDeltas.size()-1); + newDeltas.add(replacement); + } else { + newDeltas.add(currentDelta); + } + } + + return newDeltas; + } + + private DeltaMergeUtils() { + } +} diff --git a/src/com/github/difflib/text/deltamerge/InlineDeltaMergeInfo.java b/src/com/github/difflib/text/deltamerge/InlineDeltaMergeInfo.java new file mode 100644 index 00000000..cc6b399a --- /dev/null +++ b/src/com/github/difflib/text/deltamerge/InlineDeltaMergeInfo.java @@ -0,0 +1,51 @@ +/* + * Copyright 2009-2024 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.text.deltamerge; + +import java.util.List; + +import com.github.difflib.patch.AbstractDelta; + +/** + * Holds the information required to merge deltas originating from an inline + * diff + * + * @author Christian Meier + */ +public final class InlineDeltaMergeInfo { + + private final List> deltas; + private final List origList; + private final List revList; + + public InlineDeltaMergeInfo(List> deltas, List origList, List revList) { + this.deltas = deltas; + this.origList = origList; + this.revList = revList; + } + + public List> getDeltas() { + return deltas; + } + + public List getOrigList() { + return origList; + } + + public List getRevList() { + return revList; + } +} diff --git a/src/com/github/difflib/unifieddiff/UnifiedDiff.java b/src/com/github/difflib/unifieddiff/UnifiedDiff.java new file mode 100644 index 00000000..f2bb231d --- /dev/null +++ b/src/com/github/difflib/unifieddiff/UnifiedDiff.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.unifieddiff; + +import com.github.difflib.patch.PatchFailedException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +/** + * + * @author Tobias Warneke (t.warneke@gmx.net) + */ +public final class UnifiedDiff { + + private String header; + private String tail; + private final List files = new ArrayList<>(); + + public String getHeader() { + return header; + } + + public void setHeader(String header) { + this.header = header; + } + + void addFile(UnifiedDiffFile file) { + files.add(file); + } + + public List getFiles() { + return Collections.unmodifiableList(files); + } + + void setTailTxt(String tailTxt) { + this.tail = tailTxt; + } + + public String getTail() { + return tail; + } + + public List applyPatchTo(Predicate findFile, List originalLines) throws PatchFailedException { + UnifiedDiffFile file = files.stream() + .filter(diff -> findFile.test(diff.getFromFile())) + .findFirst().orElse(null); + if (file != null) { + return file.getPatch().applyTo(originalLines); + } else { + return originalLines; + } + } + + public static UnifiedDiff from(String header, String tail, UnifiedDiffFile... files) { + UnifiedDiff diff = new UnifiedDiff(); + diff.setHeader(header); + diff.setTailTxt(tail); + for (UnifiedDiffFile file : files) { + diff.addFile(file); + } + return diff; + } +} diff --git a/src/com/github/difflib/unifieddiff/UnifiedDiffFile.java b/src/com/github/difflib/unifieddiff/UnifiedDiffFile.java new file mode 100644 index 00000000..1ae3b7ca --- /dev/null +++ b/src/com/github/difflib/unifieddiff/UnifiedDiffFile.java @@ -0,0 +1,211 @@ +/* + * Copyright 2019 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.unifieddiff; + +import com.github.difflib.patch.Patch; + +/** + * Data structure for one patched file from a unified diff file. + * + * @author Tobias Warneke (t.warneke@gmx.net) + */ +public final class UnifiedDiffFile { + + private String diffCommand; + private String fromFile; + private String fromTimestamp; + private String toFile; + private String renameFrom; + private String renameTo; + private String copyFrom; + private String copyTo; + private String toTimestamp; + private String index; + private String newFileMode; + private String oldMode; + private String newMode; + private String deletedFileMode; + private String binaryAdded; + private String binaryDeleted; + private String binaryEdited; + private Patch patch = new Patch<>(); + private boolean noNewLineAtTheEndOfTheFile = false; + private Integer similarityIndex; + + public String getDiffCommand() { + return diffCommand; + } + + public void setDiffCommand(String diffCommand) { + this.diffCommand = diffCommand; + } + + public String getFromFile() { + return fromFile; + } + + public void setFromFile(String fromFile) { + this.fromFile = fromFile; + } + + public String getToFile() { + return toFile; + } + + public void setToFile(String toFile) { + this.toFile = toFile; + } + + public void setIndex(String index) { + this.index = index; + } + + public String getIndex() { + return index; + } + + public Patch getPatch() { + return patch; + } + + public String getFromTimestamp() { + return fromTimestamp; + } + + public void setFromTimestamp(String fromTimestamp) { + this.fromTimestamp = fromTimestamp; + } + + public String getToTimestamp() { + return toTimestamp; + } + + public void setToTimestamp(String toTimestamp) { + this.toTimestamp = toTimestamp; + } + + public Integer getSimilarityIndex() { + return similarityIndex; + } + + public void setSimilarityIndex(Integer similarityIndex) { + this.similarityIndex = similarityIndex; + } + + public String getRenameFrom() { + return renameFrom; + } + + public void setRenameFrom(String renameFrom) { + this.renameFrom = renameFrom; + } + + public String getRenameTo() { + return renameTo; + } + + public void setRenameTo(String renameTo) { + this.renameTo = renameTo; + } + + public String getCopyFrom() { + return copyFrom; + } + + public void setCopyFrom(String copyFrom) { + this.copyFrom = copyFrom; + } + + public String getCopyTo() { + return copyTo; + } + + public void setCopyTo(String copyTo) { + this.copyTo = copyTo; + } + + public static UnifiedDiffFile from(String fromFile, String toFile, Patch patch) { + UnifiedDiffFile file = new UnifiedDiffFile(); + file.setFromFile(fromFile); + file.setToFile(toFile); + file.patch = patch; + return file; + } + + public void setNewFileMode(String newFileMode) { + this.newFileMode = newFileMode; + } + + public String getNewFileMode() { + return newFileMode; + } + + public String getDeletedFileMode() { + return deletedFileMode; + } + + public void setDeletedFileMode(String deletedFileMode) { + this.deletedFileMode = deletedFileMode; + } + + public String getOldMode() { + return oldMode; + } + + public void setOldMode(String oldMode) { + this.oldMode = oldMode; + } + + public String getNewMode() { + return newMode; + } + + public void setNewMode(String newMode) { + this.newMode = newMode; + } + + public String getBinaryAdded() { + return binaryAdded; + } + + public void setBinaryAdded(String binaryAdded) { + this.binaryAdded = binaryAdded; + } + + public String getBinaryDeleted() { + return binaryDeleted; + } + + public void setBinaryDeleted(String binaryDeleted) { + this.binaryDeleted = binaryDeleted; + } + + public String getBinaryEdited() { + return binaryEdited; + } + + public void setBinaryEdited(String binaryEdited) { + this.binaryEdited = binaryEdited; + } + + public boolean isNoNewLineAtTheEndOfTheFile() { + return noNewLineAtTheEndOfTheFile; + } + + public void setNoNewLineAtTheEndOfTheFile(boolean noNewLineAtTheEndOfTheFile) { + this.noNewLineAtTheEndOfTheFile = noNewLineAtTheEndOfTheFile; + } +} diff --git a/src/com/github/difflib/unifieddiff/UnifiedDiffParserException.java b/src/com/github/difflib/unifieddiff/UnifiedDiffParserException.java new file mode 100644 index 00000000..ab7114db --- /dev/null +++ b/src/com/github/difflib/unifieddiff/UnifiedDiffParserException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.unifieddiff; + +/** + * + * @author Tobias Warneke (t.warneke@gmx.net) + */ +public class UnifiedDiffParserException extends RuntimeException { + + public UnifiedDiffParserException() { + } + + public UnifiedDiffParserException(String message) { + super(message); + } + + public UnifiedDiffParserException(String message, Throwable cause) { + super(message, cause); + } + + public UnifiedDiffParserException(Throwable cause) { + super(cause); + } + + public UnifiedDiffParserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/com/github/difflib/unifieddiff/UnifiedDiffReader.java b/src/com/github/difflib/unifieddiff/UnifiedDiffReader.java new file mode 100644 index 00000000..b3a66ab2 --- /dev/null +++ b/src/com/github/difflib/unifieddiff/UnifiedDiffReader.java @@ -0,0 +1,475 @@ +/* + * Copyright 2019 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.unifieddiff; + +import com.github.difflib.patch.ChangeDelta; +import com.github.difflib.patch.Chunk; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * @author Tobias Warneke (t.warneke@gmx.net) + */ +public final class UnifiedDiffReader { + + static final Pattern UNIFIED_DIFF_CHUNK_REGEXP = Pattern.compile("^@@\\s+-(?:(\\d+)(?:,(\\d+))?)\\s+\\+(?:(\\d+)(?:,(\\d+))?)\\s+@@"); + static final Pattern TIMESTAMP_REGEXP = Pattern.compile("(\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}\\.\\d{3,})(?: [+-]\\d+)?"); + + private final InternalUnifiedDiffReader READER; + private final UnifiedDiff data = new UnifiedDiff(); + + private final UnifiedDiffLine DIFF_COMMAND = new UnifiedDiffLine(true, "^diff\\s", this::processDiff); + private final UnifiedDiffLine SIMILARITY_INDEX = new UnifiedDiffLine(true, "^similarity index (\\d+)%$", this::processSimilarityIndex); + private final UnifiedDiffLine INDEX = new UnifiedDiffLine(true, "^index\\s[\\da-zA-Z]+\\.\\.[\\da-zA-Z]+(\\s(\\d+))?$", this::processIndex); + private final UnifiedDiffLine FROM_FILE = new UnifiedDiffLine(true, "^---\\s", this::processFromFile); + private final UnifiedDiffLine TO_FILE = new UnifiedDiffLine(true, "^\\+\\+\\+\\s", this::processToFile); + private final UnifiedDiffLine RENAME_FROM = new UnifiedDiffLine(true, "^rename\\sfrom\\s(.+)$", this::processRenameFrom); + private final UnifiedDiffLine RENAME_TO = new UnifiedDiffLine(true, "^rename\\sto\\s(.+)$", this::processRenameTo); + + private final UnifiedDiffLine COPY_FROM = new UnifiedDiffLine(true, "^copy\\sfrom\\s(.+)$", this::processCopyFrom); + private final UnifiedDiffLine COPY_TO = new UnifiedDiffLine(true, "^copy\\sto\\s(.+)$", this::processCopyTo); + + private final UnifiedDiffLine NEW_FILE_MODE = new UnifiedDiffLine(true, "^new\\sfile\\smode\\s(\\d+)", this::processNewFileMode); + + private final UnifiedDiffLine DELETED_FILE_MODE = new UnifiedDiffLine(true, "^deleted\\sfile\\smode\\s(\\d+)", this::processDeletedFileMode); + private final UnifiedDiffLine OLD_MODE = new UnifiedDiffLine(true, "^old\\smode\\s(\\d+)", this::processOldMode); + private final UnifiedDiffLine NEW_MODE = new UnifiedDiffLine(true, "^new\\smode\\s(\\d+)", this::processNewMode); + private final UnifiedDiffLine BINARY_ADDED = new UnifiedDiffLine(true, "^Binary\\sfiles\\s/dev/null\\sand\\sb/(.+)\\sdiffer", this::processBinaryAdded); + private final UnifiedDiffLine BINARY_DELETED = new UnifiedDiffLine(true, "^Binary\\sfiles\\sa/(.+)\\sand\\s/dev/null\\sdiffer", this::processBinaryDeleted); + private final UnifiedDiffLine BINARY_EDITED = new UnifiedDiffLine(true, "^Binary\\sfiles\\sa/(.+)\\sand\\sb/(.+)\\sdiffer", this::processBinaryEdited); + private final UnifiedDiffLine CHUNK = new UnifiedDiffLine(false, UNIFIED_DIFF_CHUNK_REGEXP, this::processChunk); + private final UnifiedDiffLine LINE_NORMAL = new UnifiedDiffLine("^\\s", this::processNormalLine); + private final UnifiedDiffLine LINE_DEL = new UnifiedDiffLine("^-", this::processDelLine); + private final UnifiedDiffLine LINE_ADD = new UnifiedDiffLine("^\\+", this::processAddLine); + + private UnifiedDiffFile actualFile; + + UnifiedDiffReader(Reader reader) { + this.READER = new InternalUnifiedDiffReader(reader); + } + + // schema = [[/^\s+/, normal], [/^diff\s/, start], [/^new file mode \d+$/, new_file], + // [/^deleted file mode \d+$/, deleted_file], [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index], + // [/^---\s/, from_file], [/^\+\+\+\s/, to_file], [/^@@\s+\-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk], + // [/^-/, del], [/^\+/, add], [/^\\ No newline at end of file$/, eof]]; + private UnifiedDiff parse() throws IOException, UnifiedDiffParserException { +// String headerTxt = ""; +// LOG.log(Level.FINE, "header parsing"); +// String line = null; +// while (READER.ready()) { +// line = READER.readLine(); +// LOG.log(Level.FINE, "parsing line {0}", line); +// if (DIFF_COMMAND.validLine(line) || INDEX.validLine(line) +// || FROM_FILE.validLine(line) || TO_FILE.validLine(line) +// || NEW_FILE_MODE.validLine(line)) { +// break; +// } else { +// headerTxt += line + "\n"; +// } +// } +// if (!"".equals(headerTxt)) { +// data.setHeader(headerTxt); +// } + + String line = READER.readLine(); + while (line != null) { + String headerTxt = ""; + LOG.log(Level.FINE, "header parsing"); + while (line != null) { + LOG.log(Level.FINE, "parsing line {0}", line); + if (validLine(line, DIFF_COMMAND, SIMILARITY_INDEX, INDEX, + FROM_FILE, TO_FILE, + RENAME_FROM, RENAME_TO, + COPY_FROM, COPY_TO, + NEW_FILE_MODE, DELETED_FILE_MODE, + OLD_MODE, NEW_MODE, + BINARY_ADDED, BINARY_DELETED, + BINARY_EDITED, CHUNK)) { + break; + } else { + headerTxt += line + "\n"; + } + line = READER.readLine(); + } + if (!"".equals(headerTxt)) { + data.setHeader(headerTxt); + } + if (line != null && !CHUNK.validLine(line)) { + initFileIfNecessary(); + while (line != null && !CHUNK.validLine(line)) { + if (!processLine(line, DIFF_COMMAND, SIMILARITY_INDEX, INDEX, + FROM_FILE, TO_FILE, + RENAME_FROM, RENAME_TO, + COPY_FROM, COPY_TO, + NEW_FILE_MODE, DELETED_FILE_MODE, + OLD_MODE, NEW_MODE, + BINARY_ADDED , BINARY_DELETED, + BINARY_EDITED)) { + throw new UnifiedDiffParserException("expected file start line not found"); + } + line = READER.readLine(); + } + } + if (line != null) { + processLine(line, CHUNK); + while ((line = READER.readLine()) != null) { + line = checkForNoNewLineAtTheEndOfTheFile(line); + + if (!processLine(line, LINE_NORMAL, LINE_ADD, LINE_DEL)) { + throw new UnifiedDiffParserException("expected data line not found"); + } + if ((originalTxt.size() == old_size && revisedTxt.size() == new_size) + || (old_size == 0 && new_size == 0 && originalTxt.size() == this.old_ln + && revisedTxt.size() == this.new_ln)) { + finalizeChunk(); + break; + } + } + line = READER.readLine(); + + line = checkForNoNewLineAtTheEndOfTheFile(line); + } + if (line == null || (line.startsWith("--") && !line.startsWith("---"))) { + break; + } + } + + if (READER.ready()) { + String tailTxt = ""; + while (READER.ready()) { + if (tailTxt.length() > 0) { + tailTxt += "\n"; + } + tailTxt += READER.readLine(); + } + data.setTailTxt(tailTxt); + } + + return data; + } + + private String checkForNoNewLineAtTheEndOfTheFile(String line) throws IOException { + if ("\\ No newline at end of file".equals(line)) { + actualFile.setNoNewLineAtTheEndOfTheFile(true); + return READER.readLine(); + } + return line; + } + + static String[] parseFileNames(String line) { + String[] split = line.split(" "); + return new String[]{ + split[2].replaceAll("^a/", ""), + split[3].replaceAll("^b/", "") + }; + } + + private static final Logger LOG = Logger.getLogger(UnifiedDiffReader.class.getName()); + + /** + * To parse a diff file use this method. + * + * @param stream This is the diff file data. + * @return In a UnifiedDiff structure this diff file data is returned. + * @throws IOException + * @throws UnifiedDiffParserException + */ + public static UnifiedDiff parseUnifiedDiff(InputStream stream) throws IOException, UnifiedDiffParserException { + UnifiedDiffReader parser = new UnifiedDiffReader(new BufferedReader(new InputStreamReader(stream))); + return parser.parse(); + } + + private boolean processLine(String line, UnifiedDiffLine... rules) throws UnifiedDiffParserException { + if (line == null) { + return false; + } + for (UnifiedDiffLine rule : rules) { + if (rule.processLine(line)) { + LOG.fine(" >>> processed rule " + rule.toString()); + return true; + } + } + LOG.warning(" >>> no rule matched " + line); + return false; + //throw new UnifiedDiffParserException("parsing error at line " + line); + } + + private boolean validLine(String line, UnifiedDiffLine ... rules) { + if (line == null) { + return false; + } + for (UnifiedDiffLine rule : rules) { + if (rule.validLine(line)) { + LOG.fine(" >>> accepted rule " + rule.toString()); + return true; + } + } + return false; + } + + private void initFileIfNecessary() { + if (!originalTxt.isEmpty() || !revisedTxt.isEmpty()) { + throw new IllegalStateException(); + } + actualFile = null; + if (actualFile == null) { + actualFile = new UnifiedDiffFile(); + data.addFile(actualFile); + } + } + + private void processDiff(MatchResult match, String line) { + //initFileIfNecessary(); + LOG.log(Level.FINE, "start {0}", line); + String[] fromTo = parseFileNames(READER.lastLine()); + actualFile.setFromFile(fromTo[0]); + actualFile.setToFile(fromTo[1]); + actualFile.setDiffCommand(line); + } + + private void processSimilarityIndex(MatchResult match, String line) { + actualFile.setSimilarityIndex(Integer.valueOf(match.group(1))); + } + + private List originalTxt = new ArrayList<>(); + private List revisedTxt = new ArrayList<>(); + private List addLineIdxList = new ArrayList<>(); + private List delLineIdxList = new ArrayList<>(); + private int old_ln; + private int old_size; + private int new_ln; + private int new_size; + private int delLineIdx = 0; + private int addLineIdx = 0; + + private void finalizeChunk() { + if (!originalTxt.isEmpty() || !revisedTxt.isEmpty()) { + actualFile.getPatch().addDelta(new ChangeDelta<>(new Chunk<>( + old_ln - 1, originalTxt, delLineIdxList), new Chunk<>( + new_ln - 1, revisedTxt, addLineIdxList))); + old_ln = 0; + new_ln = 0; + originalTxt.clear(); + revisedTxt.clear(); + addLineIdxList.clear(); + delLineIdxList.clear(); + delLineIdx = 0; + addLineIdx = 0; + } + } + + private void processNormalLine(MatchResult match, String line) { + String cline = line.substring(1); + originalTxt.add(cline); + revisedTxt.add(cline); + delLineIdx++; + addLineIdx++; + } + + private void processAddLine(MatchResult match, String line) { + String cline = line.substring(1); + revisedTxt.add(cline); + addLineIdx++; + addLineIdxList.add(new_ln - 1 + addLineIdx); + } + + private void processDelLine(MatchResult match, String line) { + String cline = line.substring(1); + originalTxt.add(cline); + delLineIdx++; + delLineIdxList.add(old_ln - 1 + delLineIdx); + } + + private void processChunk(MatchResult match, String chunkStart) { + // finalizeChunk(); + old_ln = toInteger(match, 1, 1); + old_size = toInteger(match, 2, 1); + new_ln = toInteger(match, 3, 1); + new_size = toInteger(match, 4, 1); + if (old_ln == 0) { + old_ln = 1; + } + if (new_ln == 0) { + new_ln = 1; + } + } + + private static Integer toInteger(MatchResult match, int group, int defValue) throws NumberFormatException { + return Integer.valueOf(Objects.toString(match.group(group), "" + defValue)); + } + + private void processIndex(MatchResult match, String line) { + //initFileIfNecessary(); + LOG.log(Level.FINE, "index {0}", line); + actualFile.setIndex(line.substring(6)); + } + + private void processFromFile(MatchResult match, String line) { + //initFileIfNecessary(); + actualFile.setFromFile(extractFileName(line)); + actualFile.setFromTimestamp(extractTimestamp(line)); + } + + private void processToFile(MatchResult match, String line) { + //initFileIfNecessary(); + actualFile.setToFile(extractFileName(line)); + actualFile.setToTimestamp(extractTimestamp(line)); + } + + private void processRenameFrom(MatchResult match, String line) { + actualFile.setRenameFrom(match.group(1)); + } + + private void processRenameTo(MatchResult match, String line) { + actualFile.setRenameTo(match.group(1)); + } + + private void processCopyFrom(MatchResult match, String line) { + actualFile.setCopyFrom(match.group(1)); + } + + private void processCopyTo(MatchResult match, String line) { + actualFile.setCopyTo(match.group(1)); + } + + private void processNewFileMode(MatchResult match, String line) { + //initFileIfNecessary(); + actualFile.setNewFileMode(match.group(1)); + } + + private void processDeletedFileMode(MatchResult match, String line) { + //initFileIfNecessary(); + actualFile.setDeletedFileMode(match.group(1)); + } + + private void processOldMode(MatchResult match, String line) { + actualFile.setOldMode(match.group(1)); + } + + private void processNewMode(MatchResult match, String line) { + actualFile.setNewMode(match.group(1)); + } + + private void processBinaryAdded(MatchResult match, String line) { + actualFile.setBinaryAdded(match.group(1)); + } + + private void processBinaryDeleted(MatchResult match, String line) { + actualFile.setBinaryDeleted(match.group(1)); + } + + private void processBinaryEdited(MatchResult match, String line) { + actualFile.setBinaryEdited(match.group(1)); + } + + private String extractFileName(String _line) { + Matcher matcher = TIMESTAMP_REGEXP.matcher(_line); + String line = _line; + if (matcher.find()) { + line = line.substring(0, matcher.start()); + } + line = line.split("\t")[0]; + return line.substring(4).replaceFirst("^(a|b|old|new)/", "") + .trim(); + } + + private String extractTimestamp(String line) { + Matcher matcher = TIMESTAMP_REGEXP.matcher(line); + if (matcher.find()) { + return matcher.group(); + } + return null; + } + + final class UnifiedDiffLine { + + private final Pattern pattern; + private final BiConsumer command; + private final boolean stopsHeaderParsing; + + public UnifiedDiffLine(String pattern, BiConsumer command) { + this(false, pattern, command); + } + + public UnifiedDiffLine(boolean stopsHeaderParsing, String pattern, BiConsumer command) { + this.pattern = Pattern.compile(pattern); + this.command = command; + this.stopsHeaderParsing = stopsHeaderParsing; + } + + public UnifiedDiffLine(boolean stopsHeaderParsing, Pattern pattern, BiConsumer command) { + this.pattern = pattern; + this.command = command; + this.stopsHeaderParsing = stopsHeaderParsing; + } + + public boolean validLine(String line) { + Matcher m = pattern.matcher(line); + return m.find(); + } + + public boolean processLine(String line) throws UnifiedDiffParserException { + Matcher m = pattern.matcher(line); + if (m.find()) { + command.accept(m.toMatchResult(), line); + return true; + } else { + return false; + } + } + + public boolean isStopsHeaderParsing() { + return stopsHeaderParsing; + } + + @Override + public String toString() { + return "UnifiedDiffLine{" + "pattern=" + pattern + ", stopsHeaderParsing=" + stopsHeaderParsing + '}'; + } + } +} + +class InternalUnifiedDiffReader extends BufferedReader { + + private String lastLine; + + public InternalUnifiedDiffReader(Reader reader) { + super(reader); + } + + @Override + public String readLine() throws IOException { + lastLine = super.readLine(); + return lastLine(); + } + + String lastLine() { + return lastLine; + } +} diff --git a/src/com/github/difflib/unifieddiff/UnifiedDiffWriter.java b/src/com/github/difflib/unifieddiff/UnifiedDiffWriter.java new file mode 100644 index 00000000..7cac8a2c --- /dev/null +++ b/src/com/github/difflib/unifieddiff/UnifiedDiffWriter.java @@ -0,0 +1,211 @@ +/* + * Copyright 2019 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.difflib.unifieddiff; + +import com.github.difflib.patch.AbstractDelta; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @todo use an instance to store contextSize and originalLinesProvider. + * @author Tobias Warneke (t.warneke@gmx.net) + */ +public class UnifiedDiffWriter { + + private static final Logger LOG = Logger.getLogger(UnifiedDiffWriter.class.getName()); + + public static void write(UnifiedDiff diff, Function> originalLinesProvider, Writer writer, int contextSize) throws IOException { + Objects.requireNonNull(originalLinesProvider, "original lines provider needs to be specified"); + write(diff, originalLinesProvider, line -> { + try { + writer.append(line).append("\n"); + } catch (IOException ex) { + LOG.log(Level.SEVERE, null, ex); + } + }, contextSize); + } + + public static void write(UnifiedDiff diff, Function> originalLinesProvider, Consumer writer, int contextSize) throws IOException { + if (diff.getHeader() != null) { + writer.accept(diff.getHeader()); + } + + for (UnifiedDiffFile file : diff.getFiles()) { + List> patchDeltas = new ArrayList<>( + file.getPatch().getDeltas()); + if (!patchDeltas.isEmpty()) { + writeOrNothing(writer, file.getDiffCommand()); + if (file.getIndex() != null) { + writer.accept("index " + file.getIndex()); + } + + writer.accept("--- " + (file.getFromFile() == null ? "/dev/null" : file.getFromFile())); + + if (file.getToFile() != null) { + writer.accept("+++ " + file.getToFile()); + } + + List originalLines = originalLinesProvider.apply(file.getFromFile()); + + List> deltas = new ArrayList<>(); + + AbstractDelta delta = patchDeltas.get(0); + deltas.add(delta); // add the first Delta to the current set + // if there's more than 1 Delta, we may need to output them together + if (patchDeltas.size() > 1) { + for (int i = 1; i < patchDeltas.size(); i++) { + int position = delta.getSource().getPosition(); + + // Check if the next Delta is too close to the current + // position. + // And if it is, add it to the current set + AbstractDelta nextDelta = patchDeltas.get(i); + if ((position + delta.getSource().size() + contextSize) >= (nextDelta + .getSource().getPosition() - contextSize)) { + deltas.add(nextDelta); + } else { + // if it isn't, output the current set, + // then create a new set and add the current Delta to + // it. + processDeltas(writer, originalLines, deltas, contextSize, false); + deltas.clear(); + deltas.add(nextDelta); + } + delta = nextDelta; + } + + } + // don't forget to process the last set of Deltas + processDeltas(writer, originalLines, deltas, contextSize, + patchDeltas.size() == 1 && file.getFromFile() == null); + } + + } + if (diff.getTail() != null) { + writer.accept("--"); + writer.accept(diff.getTail()); + } + } + + private static void processDeltas(Consumer writer, + List origLines, List> deltas, + int contextSize, boolean newFile) { + List buffer = new ArrayList<>(); + int origTotal = 0; // counter for total lines output from Original + int revTotal = 0; // counter for total lines output from Original + int line; + + AbstractDelta curDelta = deltas.get(0); + + int origStart; + if (newFile) { + origStart = 0; + } else { + // NOTE: +1 to overcome the 0-offset Position + origStart = curDelta.getSource().getPosition() + 1 - contextSize; + if (origStart < 1) { + origStart = 1; + } + } + + int revStart = curDelta.getTarget().getPosition() + 1 - contextSize; + if (revStart < 1) { + revStart = 1; + } + + // find the start of the wrapper context code + int contextStart = curDelta.getSource().getPosition() - contextSize; + if (contextStart < 0) { + contextStart = 0; // clamp to the start of the file + } + + // output the context before the first Delta + for (line = contextStart; line < curDelta.getSource().getPosition() + && line < origLines.size(); line++) { // + buffer.add(" " + origLines.get(line)); + origTotal++; + revTotal++; + } + // output the first Delta + getDeltaText(txt -> buffer.add(txt), curDelta); + origTotal += curDelta.getSource().getLines().size(); + revTotal += curDelta.getTarget().getLines().size(); + + int deltaIndex = 1; + while (deltaIndex < deltas.size()) { // for each of the other Deltas + AbstractDelta nextDelta = deltas.get(deltaIndex); + int intermediateStart = curDelta.getSource().getPosition() + + curDelta.getSource().getLines().size(); + for (line = intermediateStart; line < nextDelta.getSource().getPosition() + && line < origLines.size(); line++) { + // output the code between the last Delta and this one + buffer.add(" " + origLines.get(line)); + origTotal++; + revTotal++; + } + getDeltaText(txt -> buffer.add(txt), nextDelta); // output the Delta + origTotal += nextDelta.getSource().getLines().size(); + revTotal += nextDelta.getTarget().getLines().size(); + curDelta = nextDelta; + deltaIndex++; + } + + // Now output the post-Delta context code, clamping the end of the file + contextStart = curDelta.getSource().getPosition() + + curDelta.getSource().getLines().size(); + for (line = contextStart; (line < (contextStart + contextSize)) + && (line < origLines.size()); line++) { + buffer.add(" " + origLines.get(line)); + origTotal++; + revTotal++; + } + + // Create and insert the block header, conforming to the Unified Diff + // standard + writer.accept("@@ -" + origStart + "," + origTotal + " +" + revStart + "," + revTotal + " @@"); + buffer.forEach(txt -> { + writer.accept(txt); + }); + } + + /** + * getDeltaText returns the lines to be added to the Unified Diff text from the Delta parameter. + * + * @param writer consumer for the list of String lines of code + * @param delta the Delta to output + */ + private static void getDeltaText(Consumer writer, AbstractDelta delta) { + for (String line : delta.getSource().getLines()) { + writer.accept("-" + line); + } + for (String line : delta.getTarget().getLines()) { + writer.accept("+" + line); + } + } + + private static void writeOrNothing(Consumer writer, String str) throws IOException { + if (str != null) { + writer.accept(str); + } + } +} diff --git a/src/com/github/difflib/unifieddiff/package-info.java b/src/com/github/difflib/unifieddiff/package-info.java new file mode 100644 index 00000000..7384d414 --- /dev/null +++ b/src/com/github/difflib/unifieddiff/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 java-diff-utils. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * This is the new implementation of UnifiedDiff Tools. This version is multi file aware. + *

+ * To read a unified diff file you should use {@link UnifiedDiffReader#parseUnifiedDiff}. + * You will get a {@link UnifiedDiff} that holds all informations about the + * diffs and the files. + *

+ * To process the UnifiedDiff use {@link UnifiedDiffWriter#write}. + */ +package com.github.difflib.unifieddiff;