сравнение
This commit is contained in:
2025-03-25 18:56:18 +03:00
parent 809b1f8d14
commit a47a6705d8
41 changed files with 4705 additions and 107 deletions

View File

@@ -23,6 +23,7 @@
<element id="extracted-dir" path="$PROJECT_DIR$/libs/log4j-1.2.17.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/libs/xmlbeans-2.6.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/libs/commons-lang-2.6.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/libs/java-diff-utils.jar" path-in-jar="/" />
</root>
</artifact>
</component>

49
.idea/workspace.xml generated
View File

@@ -7,9 +7,46 @@
</component>
<component name="ChangeListManager">
<list default="true" id="e42177c3-2328-4b27-8a01-35779b2beb99" name="Default Changelist" comment="">
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/DiffUtils.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/UnifiedDiffUtils.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/algorithm/Change.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/algorithm/DiffAlgorithmFactory.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/algorithm/DiffAlgorithmI.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/algorithm/DiffAlgorithmListener.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/algorithm/myers/MyersDiff.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/algorithm/myers/MyersDiffWithLinearSpace.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/algorithm/myers/PathNode.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/AbstractDelta.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/ChangeDelta.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/Chunk.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/ConflictOutput.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/DeleteDelta.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/DeltaType.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/DiffException.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/EqualDelta.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/InsertDelta.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/Patch.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/PatchFailedException.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/patch/VerifyChunk.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/text/DiffRow.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/text/DiffRowGenerator.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/text/StringUtils.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/text/deltamerge/DeltaMergeUtils.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/text/deltamerge/InlineDeltaMergeInfo.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/unifieddiff/UnifiedDiff.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/unifieddiff/UnifiedDiffFile.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/unifieddiff/UnifiedDiffParserException.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/unifieddiff/UnifiedDiffReader.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/unifieddiff/UnifiedDiffWriter.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/com/github/difflib/unifieddiff/package-info.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/artifacts/VisualSapfor_jar.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/artifacts/VisualSapfor_jar.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/properties" beforeDir="false" afterPath="$PROJECT_DIR$/properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/_VisualDVM/Constants.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/_VisualDVM/Constants.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/_VisualDVM/TestingSystem/Common/TestsDatabase.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/_VisualDVM/TestingSystem/Common/TestsDatabase.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/_VisualDVM/Passes/All/TestPass.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/_VisualDVM/Passes/All/TestPass.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/_VisualDVM/Visual/Menus/MainMenuBar/MainMenuBar.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/_VisualDVM/Visual/Menus/MainMenuBar/MainMenuBar.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/_VisualDVM/Visual/Menus/MainMenuBar/VisualiserSettingsMenu/VersionsComparisonMenu.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/_VisualDVM/Visual/Menus/MainMenuBar/VisualiserSettingsMenu/VersionsComparisonMenu.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/_VisualDVM/Visual/Windows/ComparisonForm.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/_VisualDVM/Visual/Windows/ComparisonForm.java" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -89,8 +126,8 @@
<property name="UI_DESIGNER_EDITOR_MODE.UIDesignerToolWindowManager.SHOW" value="true" />
<property name="UI_DESIGNER_EDITOR_MODE.UIDesignerToolWindowManager.WIDTH" value="509" />
<property name="extract.method.default.visibility" value="public" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/src/icons/Transformations" />
<property name="project.structure.last.edited" value="Libraries" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/src" />
<property name="project.structure.last.edited" value="Artifacts" />
<property name="project.structure.proportion" value="0.15" />
<property name="project.structure.side.proportion" value="0.27322906" />
<property name="run.code.analysis.last.selected.profile" value="pProject Default" />
@@ -102,11 +139,11 @@
<recent name="controls.Trees" />
</key>
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\src" />
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\src\_dif_utils" />
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\libs" />
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\src\icons\Transformations" />
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\src\_VisualDVM\TestingSystem\DVM\DVMTasks\UI" />
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\src\_VisualDVM\Passes\All" />
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\src\icons" />
<recent name="C:\Users\misha\Documents\visual_sapfor_2023\src\icons\versions" />
</key>
<key name="MoveMembersDialog.RECENTS_KEY">
<recent name="_VisualDVM.ComponentsServer.Component.Sapfor.Sapfor" />

View File

@@ -4,7 +4,7 @@
"ServerUserPassword": "mprit_2011",
"OfferRegistrationOnStart": true,
"Workspace": "E:\\Tests",
"ProjectsSearchDirectory": "E:\\BUG\\JAC",
"ProjectsSearchDirectory": "E:\\Tests\\Downloads\\bugreport_1742890241",
"DocumentsDirectory": "C:\\Users\\misha\\Documents\\_testing_system",
"VisualiserPath": "C:\\Users\\misha\\Downloads",
"Sapfor_FPath": "E:\\_sapfor_x64\\Components\\Sapfor_F",

View File

@@ -3,7 +3,7 @@ import Common.Utils.Vector_;
import java.util.Vector;
public class Constants {
public static final int version = 1226;
public static final int version = 1227;
public static final int planner_version = 24;
public static final int testingMaxKernels = 64;
//--

View File

@@ -1,11 +1,14 @@
package _VisualDVM.Passes.All;
import Common.Passes.Pass;
import Common.Utils.Vector_;
import com.github.difflib.text.DiffRow;
import com.github.difflib.text.DiffRowGenerator;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
public class TestPass extends Pass {
@@ -52,5 +55,23 @@ public class TestPass extends Pass {
System.out.println("Square of " + num + " is " + square);
})
).join();
System.out.println("DONE");
//----
DiffRowGenerator generator = DiffRowGenerator.create()
.showInlineDiffs(true)
.inlineDiffByWord(true)
.oldTag(f -> "~")
.newTag(f -> "**")
.build();
List<DiffRow> rows = generator.generateDiffRows(
Arrays.asList("This is a test senctence.", "This is the second line.", "And here is the finish."),
Arrays.asList("This is a test for diffutils.", "This is the second line."));
System.out.println("|original|new|");
System.out.println("|--------|---|");
for (DiffRow row : rows) {
System.out.println("|" + row.getOldLine() + "|" + row.getNewLine() + "|");
}
}
}

View File

@@ -67,7 +67,7 @@ public class MainMenuBar extends VisualiserMenuBar {
setPreferredSize(new Dimension(0, 30));
//--
/*
add(new MenuBarButton() {
{
setIcon("/Common/icons/Apply.png");
@@ -77,7 +77,6 @@ public class MainMenuBar extends VisualiserMenuBar {
});
}
});
*/
//--
add(new JSeparator());
add(MachineButton = new MenuBarButton() {

View File

@@ -5,10 +5,10 @@ public class VersionsComparisonMenu extends PropertiesSubmenu {
public VersionsComparisonMenu() {
super("Сравнение версий", null,
Global.normalProperties,
"RegisterOn",
"SpacesOn",
"EmptyLinesOn",
"FortranWrapsOn",
// "RegisterOn",
// "SpacesOn",
// "EmptyLinesOn",
// "FortranWrapsOn",
"ExtensionsOn",
"ComparsionDiffMergeOn"
);

View File

@@ -51,30 +51,6 @@
<constraints/>
<properties/>
</component>
<component id="75549" class="javax.swing.JButton" binding="bPrevious">
<constraints/>
<properties>
<borderPainted value="false"/>
<icon value="Common/icons/Previous.png"/>
<maximumSize width="30" height="30"/>
<minimumSize width="30" height="30"/>
<preferredSize width="30" height="30"/>
<text value=""/>
<toolTipText value="Перейти к предыдущему различию "/>
</properties>
</component>
<component id="f8e56" class="javax.swing.JButton" binding="bNext">
<constraints/>
<properties>
<borderPainted value="false"/>
<icon value="Common/icons/Next.png"/>
<maximumSize width="30" height="30"/>
<minimumSize width="30" height="30"/>
<preferredSize width="30" height="30"/>
<text value=""/>
<toolTipText value="Перейти к следующему различию"/>
</properties>
</component>
<component id="42c3c" class="javax.swing.JButton" binding="bCompare">
<constraints/>
<properties>

View File

@@ -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<T> {
public Class<T> t; //класс объектов.
//-->>
public Vector<String> lines = new Vector<>(); //строки с учетом/неучетом пробелов. для сравнения
public Vector<String> visible_lines = new Vector<>(); //строки с нетронутыми пробелами. для отображения
//подсветка.
public LinkedHashMap<Integer, Pair<Integer, Boolean>> colors = new LinkedHashMap<>();
public RSyntaxTextAreaHighlighter slave_highlighter = null; //погонщик рабов
protected JToolBar tools;
protected JLabel lObjectName;
@@ -39,8 +43,6 @@ public abstract class ComparisonForm<T> {
//-->>
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 = 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<T> {
});
//</editor-fold>
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<T> {
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<T> {
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<T> {
Vector<String> t1 = new Vector<>();
Vector<String> 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<DiffRow> 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("");

View File

@@ -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 <T> 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 <T> Patch<T> diff(List<T> original, List<T> 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 <T> 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 <T> Patch<T> diff(List<T> original, List<T> revised) {
return DiffUtils.diff(original, revised, DEFAULT_DIFF.create(), null);
}
/**
* Computes the difference between two sequences of elements using the default diff algorithm.
*
* @param <T> 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 <T> Patch<T> diff(List<T> original, List<T> 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<String> 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 <T> Patch<T> diff(List<T> source, List<T> target,
BiPredicate<T, T> equalizer) {
if (equalizer != null) {
return DiffUtils.diff(source, target,
DEFAULT_DIFF.create(equalizer));
}
return DiffUtils.diff(source, target, new MyersDiff<>());
}
public static <T> Patch<T> diff(List<T> original, List<T> revised,
DiffAlgorithmI<T> 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 <T> Patch<T> diff(List<T> original, List<T> revised,
DiffAlgorithmI<T> 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 <T> Patch<T> diff(List<T> original, List<T> revised, DiffAlgorithmI<T> 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<String> diffInline(String original, String revised) {
List<String> origList = new ArrayList<>();
List<String> revList = new ArrayList<>();
for (Character character : original.toCharArray()) {
origList.add(character.toString());
}
for (Character character : revised.toCharArray()) {
revList.add(character.toString());
}
Patch<String> patch = DiffUtils.diff(origList, revList);
for (AbstractDelta<String> 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 <T> List<T> patch(List<T> original, Patch<T> 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 <T> List<T> unpatch(List<T> revised, Patch<T> patch) {
return patch.restore(revised);
}
private static List<String> compressLines(List<String> lines, String delimiter) {
if (lines.isEmpty()) {
return Collections.emptyList();
}
return Collections.singletonList(String.join(delimiter, lines));
}
private DiffUtils() {
}
}

View File

@@ -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<String> parseUnifiedDiff(List<String> diff) {
boolean inPrelude = true;
List<String[]> rawChunk = new ArrayList<>();
Patch<String> 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<String[]> rawChunk, Patch<String> patch, int old_ln, int new_ln) {
String tag;
String rest;
if (!rawChunk.isEmpty()) {
List<String> oldChunkLines = new ArrayList<>();
List<String> newChunkLines = new ArrayList<>();
List<Integer> removePosition = new ArrayList<>();
List<Integer> 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<String> generateUnifiedDiff(String originalFileName,
String revisedFileName, List<String> originalLines, Patch<String> patch,
int contextSize) {
if (!patch.getDeltas().isEmpty()) {
List<String> ret = new ArrayList<>();
ret.add("--- " + Optional.ofNullable(originalFileName).orElse(NULL_FILE_INDICATOR));
ret.add("+++ " + Optional.ofNullable(revisedFileName).orElse(NULL_FILE_INDICATOR));
List<AbstractDelta<String>> patchDeltas = new ArrayList<>(
patch.getDeltas());
// code outside the if block also works for single-delta issues.
List<AbstractDelta<String>> deltas = new ArrayList<>(); // current
// list
// of
// Delta's to
// process
AbstractDelta<String> 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<String> 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<String> 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<String> 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<String> processDeltas(List<String> origLines,
List<AbstractDelta<String>> deltas, int contextSize, boolean newFile) {
List<String> 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<String> 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<String> 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<String> getDeltaText(AbstractDelta<String> delta) {
List<String> 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<String> generateOriginalAndDiff(List<String> original, List<String> 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<String> generateOriginalAndDiff(List<String> original, List<String> revised, String originalFileName, String revisedFileName) {
String originalFileNameTemp = originalFileName;
String revisedFileNameTemp = revisedFileName;
if (originalFileNameTemp == null) {
originalFileNameTemp = "original";
}
if (revisedFileNameTemp == null) {
revisedFileNameTemp = "revised";
}
Patch<String> patch = DiffUtils.diff(original, revised);
List<String> 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<String> originalWithPrefix = original.stream().map(v -> " " + v).collect(Collectors.toList());
return insertOrig(originalWithPrefix, unifiedDiff);
}
//Insert the diff format to the original file
private static List<String> insertOrig(List<String> original, List<String> unifiedDiff) {
List<String> result = new ArrayList<>();
List<List<String>> diffList = new ArrayList<>();
List<String> 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<String> twoList = new ArrayList<>();
twoList.addAll(diff);
diffList.add(twoList);
diff.clear();
diff.add(u);
continue;
}
if (i == unifiedDiff.size() - 1) {
diff.add(u);
List<String> 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<List<String>> diffList, List<String> result, List<String> original) {
for (int i = 0; i < diffList.size(); i++) {
List<String> diff = diffList.get(i);
List<String> 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<String, Integer> map = getRowMap(simb);
if (null != nexSimb) {
Map<String, Integer> 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<String> result, List<String> 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<String, Integer> getRowMap(String str) {
Map<String, Integer> 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<String> getOrigList(List<String> originalWithPrefix, int start, int end) {
List<String> 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;
}
}

View File

@@ -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 <a href="t.warneke@gmx.net">Tobias Warneke</a>
*/
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);
}
}

View File

@@ -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 {
<T> DiffAlgorithmI<T> create();
<T> DiffAlgorithmI<T> create(BiPredicate<T, T> equalizer);
}

View File

@@ -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 <T> type of data that is diffed.
*/
public interface DiffAlgorithmI<T> {
/**
* 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<Change> computeDiff(List<T> source, List<T> target, DiffAlgorithmListener progress);
/**
* Simple extension to compute a changeset using arrays.
*
* @param source
* @param target
* @param progress
* @return
*/
default List<Change> computeDiff(T[] source, T[] target, DiffAlgorithmListener progress) {
return computeDiff(Arrays.asList(source), Arrays.asList(target), progress);
}
}

View File

@@ -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();
}

View File

@@ -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<T> implements DiffAlgorithmI<T> {
private final BiPredicate<T, T> equalizer;
public MyersDiff() {
equalizer = Object::equals;
}
public MyersDiff(final BiPredicate<T, T> 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<Change> computeDiff(final List<T> source, final List<T> 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<Change> 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<T> orig, final List<T> 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<Change> buildRevision(PathNode actualPath, List<T> orig, List<T> 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<Change> 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 <T> DiffAlgorithmI<T>
create() {
return new MyersDiff<>();
}
@Override
public <T> DiffAlgorithmI<T>
create(BiPredicate < T, T > equalizer) {
return new MyersDiff<>(equalizer);
}
};
}
}

View File

@@ -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<T> implements DiffAlgorithmI<T> {
private final BiPredicate<T, T> equalizer;
public MyersDiffWithLinearSpace() {
equalizer = Object::equals;
}
public MyersDiffWithLinearSpace(final BiPredicate<T, T> equalizer) {
Objects.requireNonNull(equalizer, "equalizer must not be null");
this.equalizer = equalizer;
}
@Override
public List<Change> computeDiff(List<T> source, List<T> 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<Integer> 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<Change> script;
final List<T> source;
final List<T> target;
public DiffData(List<T> source, List<T> 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 <T> DiffAlgorithmI<T>
create() {
return new MyersDiffWithLinearSpace<>();
}
@Override
public <T> DiffAlgorithmI<T>
create(BiPredicate < T, T > equalizer) {
return new MyersDiffWithLinearSpace<>(equalizer);
}
};
}
}

View File

@@ -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 <a href="mailto:juanco@suigeneris.org">Juanco Anez</a>
*/
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?
* <p>
* 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 <code>null</code> 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();
}
}

View File

@@ -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<T> implements Serializable {
private final Chunk<T> source;
private final Chunk<T> target;
private final DeltaType type;
public AbstractDelta(DeltaType type, Chunk<T> source, Chunk<T> target) {
Objects.requireNonNull(source);
Objects.requireNonNull(target);
Objects.requireNonNull(type);
this.type = type;
this.source = source;
this.target = target;
}
public Chunk<T> getSource() {
return source;
}
public Chunk<T> 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<T> target) throws PatchFailedException {
return getSource().verifyChunk(target);
}
protected VerifyChunk verifyAndApplyTo(List<T> target) throws PatchFailedException {
final VerifyChunk verify = verifyChunkToFitTarget(target);
if (verify == VerifyChunk.OK) {
applyTo(target);
}
return verify;
}
protected abstract void applyTo(List<T> target) throws PatchFailedException;
protected abstract void restore(List<T> 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 <a href="https://www.gnu.org/software/diffutils/manual/html_node/Inexact.html">Description of Fuzzy Patch</a> for more information.
*/
@SuppressWarnings("RedundantThrows")
protected void applyFuzzyToAt(List<T> 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<T> withChunks(Chunk<T> original, Chunk<T> 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;
}
}

View File

@@ -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 <a href="dm.naumenko@gmail.com">Dmitry Naumenko</a>
* @param <T> The type of the compared elements in the data 'lines'.
*/
public final class ChangeDelta<T> extends AbstractDelta<T> {
/**
* 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<T> source, Chunk<T> 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<T> 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<T> 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<T> 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<T> withChunks(Chunk<T> original, Chunk<T> revised) {
return new ChangeDelta<T>(original, revised);
}
}

View File

@@ -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
*
* <p>
* Text is represented as <code>Object[]</code> 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.
* </p>
*
* @author <a href="dm.naumenko@gmail.com>Dmitry Naumenko</a>
* @param <T> The type of the compared elements in the 'lines'.
*/
public final class Chunk<T> implements Serializable {
private final int position;
private List<T> lines;
private final List<Integer> changePosition;
/**
* Creates a chunk and saves a copy of affected lines
*
* @param position the start position
* @param lines the affected lines
* @param changePosition the positions of changed lines
*/
public Chunk(int position, List<T> lines, List<Integer> changePosition) {
this.position = position;
this.lines = new ArrayList<>(lines);
this.changePosition = changePosition != null ? new ArrayList<>(changePosition) : null;
}
/**
* Creates a chunk and saves a copy of affected lines
*
* @param position the start position
* @param lines the affected lines
*/
public Chunk(int position, List<T> lines) {
this(position, lines, null);
}
/**
* Creates a chunk and saves a copy of affected lines
*
* @param position the start position
* @param lines the affected lines
* @param changePosition the positions of changed lines
*/
public Chunk(int position, T[] lines, List<Integer> changePosition) {
this.position = position;
this.lines = Arrays.asList(lines);
this.changePosition = changePosition != null ? new ArrayList<>(changePosition) : null;
}
/**
* Creates a chunk and saves a copy of affected lines
*
* @param position the start position
* @param lines the affected lines
*/
public Chunk(int position, T[] lines) {
this(position, lines, null);
}
/**
* Verifies that this chunk's saved text matches the corresponding text in
* the given sequence.
*
* @param target the sequence to verify against.
* @throws PatchFailedException
*/
public VerifyChunk verifyChunk(List<T> target) throws PatchFailedException {
return verifyChunk(target, 0, getPosition());
}
/**
* Verifies that this chunk's saved text matches the corresponding text in
* the given sequence.
*
* @param target the sequence to verify against.
* @param fuzz the count of ignored prefix/suffix
* @param position the position of target
* @throws PatchFailedException
*/
public VerifyChunk verifyChunk(List<T> target, int fuzz, int position) throws PatchFailedException {
//noinspection UnnecessaryLocalVariable
int startIndex = fuzz;
int lastIndex = size() - fuzz;
int last = position + size() - 1;
if (position + fuzz > target.size() || last - fuzz > target.size()) {
return VerifyChunk.POSITION_OUT_OF_TARGET;
}
for (int i = startIndex; i < lastIndex; i++) {
if (!target.get(position + i).equals(lines.get(i))) {
return VerifyChunk.CONTENT_DOES_NOT_MATCH_TARGET;
}
}
return VerifyChunk.OK;
}
/**
* @return the start position of chunk in the text
*/
public int getPosition() {
return position;
}
public void setLines(List<T> lines) {
this.lines = lines;
}
/**
* @return the affected lines
*/
public List<T> getLines() {
return lines;
}
/**
* @return the positions of changed lines of chunk in the text
*/
public List<Integer> getChangePosition() {
return changePosition;
}
public int size() {
return lines.size();
}
/**
* Returns the index of the last line of the chunk.
*/
public int last() {
return getPosition() + size() - 1;
}
@Override
public int hashCode() {
return Objects.hash(lines, position, size());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Chunk<?> other = (Chunk<?>) obj;
if (lines == null) {
if (other.lines != null) {
return false;
}
} else if (!lines.equals(other.lines)) {
return false;
}
return position == other.position;
}
@Override
public String toString() {
return "[position: " + position + ", size: " + size() + ", lines: " + lines + "]";
}
}

View File

@@ -0,0 +1,33 @@
/*-
* #%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 java.io.Serializable;
import java.util.List;
/**
*
* @author tw
*/
@FunctionalInterface
public interface ConflictOutput<T> extends Serializable {
public void processConflict(VerifyChunk verifyChunk, AbstractDelta<T> delta, List<T> result) throws PatchFailedException;
}

View File

@@ -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 <a href="dm.naumenko@gmail.com">Dmitry Naumenko</a>
* @param <T> The type of the compared elements in the 'lines'.
*/
public final class DeleteDelta<T> extends AbstractDelta<T> {
/**
* 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<T> original, Chunk<T> revised) {
super(DeltaType.DELETE, original, revised);
}
@Override
protected void applyTo(List<T> 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<T> target) {
int position = this.getTarget().getPosition();
List<T> 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<T> withChunks(Chunk<T> original, Chunk<T> revised) {
return new DeleteDelta<T>(original, revised);
}
}

View File

@@ -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
}

View File

@@ -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 <a href="mailto:juanco@suigeneris.org">Juanco Anez</a>
*/
public class DiffException extends Exception {
private static final long serialVersionUID = 1L;
public DiffException() {
}
public DiffException(String msg) {
super(msg);
}
}

View File

@@ -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<T> extends AbstractDelta<T> {
public EqualDelta(Chunk<T> source, Chunk<T> target) {
super(DeltaType.EQUAL, source, target);
}
@Override
protected void applyTo(List<T> target) throws PatchFailedException {
}
@Override
protected void restore(List<T> target) {
}
/**
* {@inheritDoc}
*/
@Override
protected void applyFuzzyToAt(List<T> target, int fuzz, int delta) {
// equals so no operations
}
@Override
public String toString() {
return "[EqualDelta, position: " + getSource().getPosition() + ", lines: "
+ getSource().getLines() + "]";
}
@Override
public AbstractDelta<T> withChunks(Chunk<T> original, Chunk<T> revised) {
return new EqualDelta<T>(original, revised);
}
}

View File

@@ -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 <a href="dm.naumenko@gmail.com">Dmitry Naumenko</a>
* @param <T> The type of the compared elements in the 'lines'.
*/
public final class InsertDelta<T> extends AbstractDelta<T> {
/**
* 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<T> original, Chunk<T> revised) {
super(DeltaType.INSERT, original, revised);
}
@Override
protected void applyTo(List<T> target) throws PatchFailedException {
int position = this.getSource().getPosition();
List<T> lines = this.getTarget().getLines();
for (int i = 0; i < lines.size(); i++) {
target.add(position + i, lines.get(i));
}
}
@Override
protected void restore(List<T> 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<T> withChunks(Chunk<T> original, Chunk<T> revised) {
return new InsertDelta<T>(original, revised);
}
}

View File

@@ -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 <a href="dm.naumenko@gmail.com">Dmitry Naumenko</a>
* @param <T> The type of the compared elements in the 'lines'.
*/
public final class Patch<T> implements Serializable {
private final List<AbstractDelta<T>> 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<T> applyTo(List<T> target) throws PatchFailedException {
List<T> 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<T> target) throws PatchFailedException {
ListIterator<AbstractDelta<T>> it = getDeltas().listIterator(deltas.size());
while (it.hasPrevious()) {
AbstractDelta<T> delta = it.previous();
VerifyChunk valid = delta.verifyAndApplyTo(target);
if (valid != VerifyChunk.OK) {
conflictOutput.processConflict(valid, delta, target);
}
}
}
private static class PatchApplyingContext<T> {
public final List<T> 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<T> result, int maxFuzz) {
this.result = result;
this.maxFuzz = maxFuzz;
}
}
public List<T> applyFuzzy(List<T> target, int maxFuzz) throws PatchFailedException {
PatchApplyingContext<T> ctx = new PatchApplyingContext<>(new ArrayList<>(target), maxFuzz);
// the difference between patch's position and actually applied position
int lastPatchDelta = 0;
for (AbstractDelta<T> 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<T> ctx, AbstractDelta<T> 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<T> ctx, AbstractDelta<T> 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<T> ctx, AbstractDelta<T> 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<T> CONFLICT_PRODUCES_EXCEPTION = (VerifyChunk verifyChunk, AbstractDelta<T> delta, List<T> result) -> {
throw new PatchFailedException("could not apply patch due to " + verifyChunk.toString());
};
/**
* Git like merge conflict output.
*/
public static final ConflictOutput<String> CONFLICT_PRODUCES_MERGE_CONFLICT = (VerifyChunk verifyChunk, AbstractDelta<String> delta, List<String> result) -> {
if (result.size() > delta.getSource().getPosition()) {
List<String> 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<T> 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<T> 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<T> restore(List<T> target) {
List<T> 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<T> target) {
ListIterator<AbstractDelta<T>> it = getDeltas().listIterator(deltas.size());
while (it.hasPrevious()) {
AbstractDelta<T> delta = it.previous();
delta.restore(target);
}
}
/**
* Add the given delta to this patch
*
* @param delta the given delta
*/
public void addDelta(AbstractDelta<T> delta) {
deltas.add(delta);
}
/**
* Get the list of computed deltas
*
* @return the deltas
*/
public List<AbstractDelta<T>> getDeltas() {
deltas.sort(comparing(d -> d.getSource().getPosition()));
return deltas;
}
@Override
public String toString() {
return "Patch{" + "deltas=" + deltas + '}';
}
public static <T> Patch<T> generate(List<T> original, List<T> revised, List<Change> changes) {
return generate(original, revised, changes, false);
}
private static <T> Chunk<T> buildChunk(int start, int end, List<T> data) {
return new Chunk<>(start, new ArrayList<>(data.subList(start, end)));
}
public static <T> Patch<T> generate(List<T> original, List<T> revised, List<Change> _changes, boolean includeEquals) {
Patch<T> patch = new Patch<>(_changes.size());
int startOriginal = 0;
int startRevised = 0;
List<Change> changes = _changes;
if (includeEquals) {
changes = new ArrayList<Change>(_changes);
Collections.sort(changes, comparing(d -> d.startOriginal));
}
for (Change change : changes) {
if (includeEquals && startOriginal < change.startOriginal) {
patch.addDelta(new EqualDelta<T>(
buildChunk(startOriginal, change.startOriginal, original),
buildChunk(startRevised, change.startRevised, revised)));
}
Chunk<T> orgChunk = buildChunk(change.startOriginal, change.endOriginal, original);
Chunk<T> 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<T>(
buildChunk(startOriginal, original.size(), original),
buildChunk(startRevised, revised.size(), revised)));
}
return patch;
}
}

View File

@@ -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 <a href="mailto:juanco@suigeneris.org">Juanco Anez</a>
*/
public class PatchFailedException extends DiffException {
private static final long serialVersionUID = 1L;
public PatchFailedException() {
}
public PatchFailedException(String msg) {
super(msg);
}
}

View File

@@ -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
}

View File

@@ -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 <a href="dm.naumenko@gmail.com">Dmitry Naumenko</a>
*/
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 + "]";
}
}

View File

@@ -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 <code>
* DiffRowGenerator generator = new DiffRowGenerator.Builder().showInlineDiffs(true).
* ignoreWhiteSpaces(true).columnWidth(100).build();
* </code>
*/
public final class DiffRowGenerator {
public static final BiPredicate<String, String> DEFAULT_EQUALIZER = Object::equals;
public static final BiPredicate<String, String> IGNORE_WHITESPACE_EQUALIZER = (original, revised)
-> adjustWhitespace(original).equals(adjustWhitespace(revised));
public static final Function<String, String> LINE_NORMALIZER_FOR_HTML = StringUtils::normalize;
/**
* Splitting lines by character to achieve char by char diff checking.
*/
public static final Function<String, List<String>> SPLITTER_BY_CHARACTER = line -> {
List<String> 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<String, List<String>> SPLITTER_BY_WORD = line -> splitStringPreserveDelimiter(line, SPLIT_BY_WORD_PATTERN);
public static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
public static final Function<InlineDeltaMergeInfo, List<AbstractDelta<String>>> DEFAULT_INLINE_DELTA_MERGER = InlineDeltaMergeInfo::getDeltas;
/**
* Merge diffs which are separated by equalities consisting of whitespace only.
*/
public static final Function<InlineDeltaMergeInfo, List<AbstractDelta<String>>> 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<String> splitStringPreserveDelimiter(String str, Pattern SPLIT_PATTERN) {
List<String> 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<String> sequence, int startPosition,
int endPosition, Tag tag, BiFunction<Tag, Boolean, String> tagGenerator,
Function<String, String> 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<String, String> equalizer;
private final boolean ignoreWhiteSpaces;
private final Function<String, List<String>> inlineDiffSplitter;
private final boolean mergeOriginalRevised;
private final BiFunction<Tag, Boolean, String> newTag;
private final BiFunction<Tag, Boolean, String> oldTag;
private final boolean reportLinesUnchanged;
private final Function<String, String> lineNormalizer;
private final Function<String, String> processDiffs;
private final Function<InlineDeltaMergeInfo, List<AbstractDelta<String>>> 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<DiffRow> generateDiffRows(List<String> original, List<String> 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<DiffRow> generateDiffRows(final List<String> original, Patch<String> patch) {
List<DiffRow> diffRows = new ArrayList<>();
int endPos = 0;
final List<AbstractDelta<String>> deltaList = patch.getDeltas();
if (decompressDeltas) {
for (AbstractDelta<String> originalDelta : deltaList) {
for (AbstractDelta<String> delta : decompressDeltas(originalDelta)) {
endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta);
}
}
} else {
for (AbstractDelta<String> 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<String> original, int endPos, List<DiffRow> diffRows, AbstractDelta<String> delta) {
Chunk<String> orig = delta.getSource();
Chunk<String> 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<AbstractDelta<String>> decompressDeltas(AbstractDelta<String> delta) {
if (delta.getType() == DeltaType.CHANGE && delta.getSource().size() != delta.getTarget().size()) {
List<AbstractDelta<String>> deltas = new ArrayList<>();
//System.out.println("decompress this " + delta);
int minSize = Math.min(delta.getSource().size(), delta.getTarget().size());
Chunk<String> orig = delta.getSource();
Chunk<String> rev = delta.getTarget();
deltas.add(new ChangeDelta<String>(
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<String>(
new Chunk<>(orig.getPosition() + minSize, Collections.emptyList()),
new Chunk<>(rev.getPosition() + minSize, rev.getLines().subList(minSize, rev.getLines().size()))));
} else {
deltas.add(new DeleteDelta<String>(
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<String> normalizeLines(List<String> 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<DiffRow> generateInlineDiffs(AbstractDelta<String> delta) {
List<String> orig = normalizeLines(delta.getSource().getLines());
List<String> rev = normalizeLines(delta.getTarget().getLines());
List<String> origList;
List<String> revList;
String joinedOrig = String.join("\n", orig);
String joinedRev = String.join("\n", rev);
origList = inlineDiffSplitter.apply(joinedOrig);
revList = inlineDiffSplitter.apply(joinedRev);
List<AbstractDelta<String>> originalInlineDeltas = DiffUtils.diff(origList, revList, equalizer)
.getDeltas();
List<AbstractDelta<String>> inlineDeltas = inlineDeltaMerger
.apply(new InlineDeltaMergeInfo(originalInlineDeltas, origList, revList));
Collections.reverse(inlineDeltas);
for (AbstractDelta<String> inlineDelta : inlineDeltas) {
Chunk<String> inlineOrig = inlineDelta.getSource();
Chunk<String> 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<String> original = Arrays.asList(origResult.toString().split("\n"));
List<String> revised = Arrays.asList(revResult.toString().split("\n"));
List<DiffRow> 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<Tag, Boolean, String> oldTag
= (tag, f) -> f ? "<span class=\"editOldInline\">" : "</span>";
private BiFunction<Tag, Boolean, String> newTag
= (tag, f) -> f ? "<span class=\"editNewInline\">" : "</span>";
private int columnWidth = 0;
private boolean mergeOriginalRevised = false;
private boolean reportLinesUnchanged = false;
private Function<String, List<String>> inlineDiffSplitter = SPLITTER_BY_CHARACTER;
private Function<String, String> lineNormalizer = LINE_NORMALIZER_FOR_HTML;
private Function<String, String> processDiffs = null;
private BiPredicate<String, String> equalizer = null;
private boolean replaceOriginalLinefeedInChangesWithSpaces = false;
private Function<InlineDeltaMergeInfo, List<AbstractDelta<String>>> 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<Tag, Boolean, String> 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<Boolean, String> generator) {
this.oldTag = (tag, f) -> generator.apply(f);
return this;
}
/**
* Generator for New-Text-Tags.
*
* @param generator
* @return
*/
public Builder newTag(BiFunction<Tag, Boolean, String> generator) {
this.newTag = generator;
return this;
}
/**
* Generator for New-Text-Tags.
*
* @param generator
* @return
*/
public Builder newTag(Function<Boolean, String> 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<String, String> 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 &lt; 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:
*
* <pre>
* false: (aBa : aba) -- changed: a(B)a : a(b)a
* true: (aBa : aba) -- changed: (aBa) : (aba)
* </pre>
*/
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<String, List<String>> inlineDiffSplitter) {
this.inlineDiffSplitter = inlineDiffSplitter;
return this;
}
/**
* By default DiffRowGenerator preprocesses lines for HTML output. Tabs
* and special HTML characters like "&lt;" are replaced with its encoded
* value. To change this you can provide a customized line normalizer
* here.
*
* @param lineNormalizer
* @return
*/
public Builder lineNormalizer(Function<String, String> 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<String, String> 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<InlineDeltaMergeInfo, List<AbstractDelta<String>>> inlineDeltaMerger) {
this.inlineDeltaMerger = inlineDeltaMerger;
return this;
}
}
}

View File

@@ -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 <code>&lt;</code> or <code>&gt;</code>.
*
* @param str
* @return str with some HTML meta characters escaped.
*/
public static String htmlEntites(String str) {
return str.replace("<", "&lt;").replace(">", "&gt;");
}
public static String normalize(String str) {
return htmlEntites(str).replace("\t", " ");
}
public static List<String> wrapText(List<String> 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 = "<br/>".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, "<br/>");
widthIndex += columnWidth;
}
return b.toString();
}
private StringUtils() {
}
}

View File

@@ -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 <a href="christian.meier@epictec.ch">Christian Meier</a>
*/
final public class DeltaMergeUtils {
public static List<AbstractDelta<String>> mergeInlineDeltas(InlineDeltaMergeInfo deltaMergeInfo,
Predicate<List<String>> replaceEquality) {
final List<AbstractDelta<String>> originalDeltas = deltaMergeInfo.getDeltas();
if (originalDeltas.size() < 2) {
return originalDeltas;
}
final List<AbstractDelta<String>> newDeltas = new ArrayList<>();
newDeltas.add(originalDeltas.get(0));
for (int i = 1; i < originalDeltas.size(); i++) {
final AbstractDelta<String> previousDelta = newDeltas.get(newDeltas.size()-1);
final AbstractDelta<String> currentDelta = originalDeltas.get(i);
final List<String> 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<String> allSourceLines = new ArrayList<>();
allSourceLines.addAll(previousDelta.getSource().getLines());
allSourceLines.addAll(equalities);
allSourceLines.addAll(currentDelta.getSource().getLines());
final List<String> allTargetLines = new ArrayList<>();
allTargetLines.addAll(previousDelta.getTarget().getLines());
allTargetLines.addAll(equalities);
allTargetLines.addAll(currentDelta.getTarget().getLines());
final ChangeDelta<String> 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() {
}
}

View File

@@ -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 <a href="christian.meier@epictec.ch">Christian Meier</a>
*/
public final class InlineDeltaMergeInfo {
private final List<AbstractDelta<String>> deltas;
private final List<String> origList;
private final List<String> revList;
public InlineDeltaMergeInfo(List<AbstractDelta<String>> deltas, List<String> origList, List<String> revList) {
this.deltas = deltas;
this.origList = origList;
this.revList = revList;
}
public List<AbstractDelta<String>> getDeltas() {
return deltas;
}
public List<String> getOrigList() {
return origList;
}
public List<String> getRevList() {
return revList;
}
}

View File

@@ -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<UnifiedDiffFile> 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<UnifiedDiffFile> getFiles() {
return Collections.unmodifiableList(files);
}
void setTailTxt(String tailTxt) {
this.tail = tailTxt;
}
public String getTail() {
return tail;
}
public List<String> applyPatchTo(Predicate<String> findFile, List<String> 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;
}
}

View File

@@ -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<String> 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<String> 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<String> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> originalTxt = new ArrayList<>();
private List<String> revisedTxt = new ArrayList<>();
private List<Integer> addLineIdxList = new ArrayList<>();
private List<Integer> 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<MatchResult, String> command;
private final boolean stopsHeaderParsing;
public UnifiedDiffLine(String pattern, BiConsumer<MatchResult, String> command) {
this(false, pattern, command);
}
public UnifiedDiffLine(boolean stopsHeaderParsing, String pattern, BiConsumer<MatchResult, String> command) {
this.pattern = Pattern.compile(pattern);
this.command = command;
this.stopsHeaderParsing = stopsHeaderParsing;
}
public UnifiedDiffLine(boolean stopsHeaderParsing, Pattern pattern, BiConsumer<MatchResult, String> 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;
}
}

View File

@@ -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<String, List<String>> 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<String, List<String>> originalLinesProvider, Consumer<String> writer, int contextSize) throws IOException {
if (diff.getHeader() != null) {
writer.accept(diff.getHeader());
}
for (UnifiedDiffFile file : diff.getFiles()) {
List<AbstractDelta<String>> 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<String> originalLines = originalLinesProvider.apply(file.getFromFile());
List<AbstractDelta<String>> deltas = new ArrayList<>();
AbstractDelta<String> 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<String> 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<String> writer,
List<String> origLines, List<AbstractDelta<String>> deltas,
int contextSize, boolean newFile) {
List<String> 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<String> 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<String> 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<String> writer, AbstractDelta<String> delta) {
for (String line : delta.getSource().getLines()) {
writer.accept("-" + line);
}
for (String line : delta.getTarget().getLines()) {
writer.accept("+" + line);
}
}
private static void writeOrNothing(Consumer<String> writer, String str) throws IOException {
if (str != null) {
writer.accept(str);
}
}
}

View File

@@ -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.
* <p/>
* 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.
* <p/>
* To process the UnifiedDiff use {@link UnifiedDiffWriter#write}.
*/
package com.github.difflib.unifieddiff;