initial commit: some diff algorithm
This commit is contained in:
commit
3f6a16224e
38 files changed
+3735
No files matched your search
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib
|
||||
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmFactory
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmI
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmListener
|
||||
import dev.gitlive.difflib.algorithm.myers.MeyersDiff
|
||||
import dev.gitlive.difflib.patch.Patch
|
||||
import dev.gitlive.difflib.patch.PatchFailedException
|
||||
|
||||
internal typealias Predicate<T> = (T) -> Boolean
|
||||
internal typealias BiPredicate<T, R> = (T, R) -> Boolean
|
||||
internal typealias Consumer<T> = (T) -> Unit
|
||||
internal typealias Function<T, R> = (T) -> R
|
||||
internal typealias BiFunction<T, T2, R> = (T, T2) -> R
|
||||
internal typealias BiConsumer<T, U> = (T, U) -> Unit
|
||||
internal typealias LineReader = suspend () -> String?
|
||||
|
||||
/**
|
||||
* Implements the difference and patching engine
|
||||
*/
|
||||
object DiffUtils {
|
||||
/**
|
||||
* This factory generates the DEFAULT_DIFF algorithm for all these routines.
|
||||
*/
|
||||
var DEFAULT_DIFF: DiffAlgorithmFactory = MeyersDiff.Companion.factory()
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun withDefaultDiffAlgorithmFactory(factory: DiffAlgorithmFactory) {
|
||||
DEFAULT_DIFF = factory
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the difference between the original and revised list of elements
|
||||
* with default diff algorithm
|
||||
*
|
||||
* @param <T> types to be diffed
|
||||
* @param original The original text. Must not be `null`.
|
||||
* @param revised The revised text. Must not be `null`.
|
||||
* @param progress progress listener
|
||||
* @return The patch describing the difference between the original and
|
||||
* revised sequences. Never `null`.
|
||||
</T> */
|
||||
fun <T> diff(original: List<T>, revised: List<T>, progress: DiffAlgorithmListener?): Patch<T> {
|
||||
return diff<T>(original, revised, DEFAULT_DIFF.create(), progress)
|
||||
}
|
||||
|
||||
fun <T> diff(original: List<T>, revised: List<T>): Patch<T> {
|
||||
return diff<T>(original, revised, DEFAULT_DIFF.create(), null)
|
||||
}
|
||||
|
||||
fun <T> diff(original: List<T>, revised: List<T>, includeEqualParts: Boolean): Patch<T> {
|
||||
return diff<T>(original, revised, DEFAULT_DIFF.create(), null, includeEqualParts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the difference between the original and revised text.
|
||||
*/
|
||||
fun diff(
|
||||
sourceText: String, targetText: String,
|
||||
progress: DiffAlgorithmListener?
|
||||
): Patch<String> {
|
||||
return diff(sourceText.trimEnd('\n').lines(), targetText.trimEnd('\n').lines(), progress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the difference between the original and revised list of elements
|
||||
* with default diff algorithm
|
||||
*
|
||||
* @param source The original text. Must not be `null`.
|
||||
* @param target The revised text. Must not be `null`.
|
||||
*
|
||||
* @param equalizer the equalizer object to replace the default compare
|
||||
* algorithm (Object.equals). If `null` the default equalizer of the
|
||||
* default algorithm is used..
|
||||
* @return The patch describing the difference between the original and
|
||||
* revised sequences. Never `null`.
|
||||
*/
|
||||
fun <T> diff(
|
||||
source: List<T>, target: List<T>,
|
||||
equalizer: BiPredicate<T, T>?
|
||||
): Patch<T> {
|
||||
return if (equalizer != null) {
|
||||
diff(
|
||||
source, target,
|
||||
DEFAULT_DIFF.create(equalizer)
|
||||
)
|
||||
} else diff(source, target, MeyersDiff())
|
||||
}
|
||||
|
||||
fun <T> diff(
|
||||
source: List<T>, target: List<T>,
|
||||
includeEqualParts: Boolean,
|
||||
equalizer: BiPredicate<T, T>?
|
||||
): Patch<T> {
|
||||
return if (equalizer != null) {
|
||||
diff(source, target, DEFAULT_DIFF.create(equalizer), null, includeEqualParts)
|
||||
} else diff(source, target, MeyersDiff())
|
||||
}
|
||||
|
||||
fun <T> diff(
|
||||
original: List<T>, revised: List<T>,
|
||||
algorithm: DiffAlgorithmI<T>, progress: DiffAlgorithmListener?
|
||||
): Patch<T> {
|
||||
return diff(original, revised, algorithm, progress, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the difference between the original and revised list of elements
|
||||
* with default diff algorithm
|
||||
*
|
||||
* @param original The original text. Must not be `null`.
|
||||
* @param revised The revised text. Must not be `null`.
|
||||
* @param algorithm The diff algorithm. Must not be `null`.
|
||||
* @param progress 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 `null`.
|
||||
*/
|
||||
fun <T> diff(
|
||||
original: List<T>, revised: List<T>,
|
||||
algorithm: DiffAlgorithmI<T>, progress: DiffAlgorithmListener?,
|
||||
includeEqualParts: Boolean
|
||||
): Patch<T> {
|
||||
return Patch.Companion.generate<T>(
|
||||
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 The original text. Must not be `null`.
|
||||
* @param revised The revised text. Must not be `null`.
|
||||
* @param algorithm The diff algorithm. Must not be `null`.
|
||||
* @return The patch describing the difference between the original and
|
||||
* revised sequences. Never `null`.
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun <T> diff(original: List<T>, revised: List<T>, algorithm: DiffAlgorithmI<T>): Patch<T> {
|
||||
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
|
||||
* @param revised
|
||||
* @return
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun diffInline(original: String, revised: String): Patch<String> {
|
||||
val origList: MutableList<String> = ArrayList()
|
||||
val revList: MutableList<String> = ArrayList()
|
||||
for (character in original) {
|
||||
origList.add(character.toString())
|
||||
}
|
||||
for (character in revised) {
|
||||
revList.add(character.toString())
|
||||
}
|
||||
val patch: Patch<String> = diff(origList, revList)
|
||||
for (delta in patch.getDeltas()) {
|
||||
delta.source.lines = compressLines(delta.source.lines, "")
|
||||
delta.target.lines = compressLines(delta.target.lines, "")
|
||||
}
|
||||
return patch
|
||||
}
|
||||
|
||||
private fun compressLines(lines: List<String>, delimiter: String): List<String> {
|
||||
return if (lines.isEmpty()) {
|
||||
emptyList<String>()
|
||||
} else listOf<String>(lines.joinToString(delimiter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the original text with given patch
|
||||
*
|
||||
* @param original the original text
|
||||
* @param patch the given patch
|
||||
* @return the revised text
|
||||
* @throws PatchFailedException if can't apply patch
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
// @Throws(PatchFailedException::class)
|
||||
fun <T> patch(original: List<T>, patch: Patch<T>): List<T> {
|
||||
return patch.applyTo(original)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpatch the revised text for a given patch
|
||||
*
|
||||
* @param revised the revised text
|
||||
* @param patch the given patch
|
||||
* @return the original text
|
||||
*/
|
||||
fun <T> unpatch(revised: List<T>, patch: Patch<T>): List<T> {
|
||||
return patch.restore(revised)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib
|
||||
|
||||
import dev.gitlive.difflib.patch.AbstractDelta
|
||||
import dev.gitlive.difflib.patch.ChangeDelta
|
||||
import dev.gitlive.difflib.patch.Chunk
|
||||
import dev.gitlive.difflib.patch.Patch
|
||||
|
||||
/**
|
||||
*
|
||||
* @author toben
|
||||
*/
|
||||
object UnifiedDiffUtils {
|
||||
private val UNIFIED_DIFF_CHUNK_REGEXP =
|
||||
Regex("^@@\\s+-(?:(\\d+)(?:,(\\d+))?)\\s+\\+(?:(\\d+)(?:,(\\d+))?)\\s+@@$")
|
||||
private const val 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.
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun parseUnifiedDiff(diff: List<String>): Patch<String> {
|
||||
var inPrelude = true
|
||||
val rawChunk: MutableList<Array<String>> = ArrayList()
|
||||
val patch = Patch<String>()
|
||||
var old_ln = 0
|
||||
var new_ln = 0
|
||||
var tag: String
|
||||
var rest: String
|
||||
for (line in diff) {
|
||||
// Skip leading lines until after we've seen one starting with '+++'
|
||||
if (inPrelude) {
|
||||
if (line.startsWith("+++")) {
|
||||
inPrelude = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
val m = UNIFIED_DIFF_CHUNK_REGEXP.find(line)
|
||||
if (m != null) {
|
||||
// Process the lines in the previous chunk
|
||||
processLinesInPrevChunk(rawChunk, patch, old_ln, new_ln)
|
||||
// Parse the @@ header
|
||||
old_ln = if (m.groups[1] == null) 1 else m.groupValues[1].toInt()
|
||||
new_ln = if (m.groups[3] == null) 1 else m.groupValues[3].toInt()
|
||||
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 (" " == tag || "+" == tag || "-" == tag) {
|
||||
rawChunk.add(arrayOf(tag, rest))
|
||||
}
|
||||
} else {
|
||||
rawChunk.add(arrayOf(" ", ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the lines in the last chunk
|
||||
processLinesInPrevChunk(rawChunk, patch, old_ln, new_ln)
|
||||
return patch
|
||||
}
|
||||
|
||||
private fun processLinesInPrevChunk(
|
||||
rawChunk: MutableList<Array<String>>,
|
||||
patch: Patch<String>,
|
||||
old_ln: Int,
|
||||
new_ln: Int
|
||||
) {
|
||||
var tag: String
|
||||
var rest: String
|
||||
if (!rawChunk.isEmpty()) {
|
||||
val oldChunkLines: MutableList<String> = ArrayList()
|
||||
val newChunkLines: MutableList<String> = ArrayList()
|
||||
val removePosition: MutableList<Int> = ArrayList()
|
||||
val addPosition: MutableList<Int> = ArrayList()
|
||||
var removeNum = 0
|
||||
var addNum = 0
|
||||
for (raw_line in rawChunk) {
|
||||
tag = raw_line[0]
|
||||
rest = raw_line[1]
|
||||
if (" " == tag || "-" == tag) {
|
||||
removeNum++
|
||||
oldChunkLines.add(rest)
|
||||
if ("-" == tag) {
|
||||
removePosition.add(old_ln - 1 + removeNum)
|
||||
}
|
||||
}
|
||||
if (" " == tag || "+" == tag) {
|
||||
addNum++
|
||||
newChunkLines.add(rest)
|
||||
if ("+" == tag) {
|
||||
addPosition.add(new_ln - 1 + addNum)
|
||||
}
|
||||
}
|
||||
}
|
||||
patch.addDelta(
|
||||
ChangeDelta(
|
||||
Chunk(
|
||||
old_ln - 1, oldChunkLines, removePosition
|
||||
), 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.
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun generateUnifiedDiff(
|
||||
originalFileName: String?,
|
||||
revisedFileName: String?, originalLines: List<String>, patch: Patch<String>,
|
||||
contextSize: Int
|
||||
): List<String> {
|
||||
if (!patch.getDeltas().isEmpty()) {
|
||||
val ret: MutableList<String> = ArrayList()
|
||||
ret.add("--- ${originalFileName ?: NULL_FILE_INDICATOR}")
|
||||
ret.add("+++ ${revisedFileName ?: NULL_FILE_INDICATOR}")
|
||||
val patchDeltas: List<AbstractDelta<String>> = ArrayList<AbstractDelta<String>>(
|
||||
patch.getDeltas()
|
||||
)
|
||||
|
||||
// code outside the if block also works for single-delta issues.
|
||||
val deltas: MutableList<AbstractDelta<String>> = ArrayList() // current
|
||||
// list
|
||||
// of
|
||||
// Delta's to
|
||||
// process
|
||||
var delta = patchDeltas[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 (i in 1 until patchDeltas.size) {
|
||||
val position = delta.source.position // 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
|
||||
val nextDelta = patchDeltas[i]
|
||||
if (position + delta.source.size() + contextSize >= nextDelta.source.position - 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.
|
||||
val 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
|
||||
val curBlock = processDeltas(
|
||||
originalLines, deltas,
|
||||
contextSize, patchDeltas.size == 1 && originalFileName == null
|
||||
)
|
||||
ret.addAll(curBlock)
|
||||
return ret
|
||||
}
|
||||
return 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 fun processDeltas(
|
||||
origLines: List<String>,
|
||||
deltas: List<AbstractDelta<String>>, contextSize: Int, newFile: Boolean
|
||||
): List<String> {
|
||||
val buffer: MutableList<String> = ArrayList()
|
||||
var origTotal = 0 // counter for total lines output from Original
|
||||
var revTotal = 0 // counter for total lines output from Original
|
||||
var line: Int
|
||||
var curDelta = deltas[0]
|
||||
var origStart: Int
|
||||
if (newFile) {
|
||||
origStart = 0
|
||||
} else {
|
||||
// NOTE: +1 to overcome the 0-offset Position
|
||||
origStart = curDelta.source.position + 1 - contextSize
|
||||
if (origStart < 1) {
|
||||
origStart = 1
|
||||
}
|
||||
}
|
||||
var revStart = curDelta.target.position + 1 - contextSize
|
||||
if (revStart < 1) {
|
||||
revStart = 1
|
||||
}
|
||||
|
||||
// find the start of the wrapper context code
|
||||
var contextStart = curDelta.source.position - contextSize
|
||||
if (contextStart < 0) {
|
||||
contextStart = 0 // clamp to the start of the file
|
||||
}
|
||||
|
||||
// output the context before the first Delta
|
||||
line = contextStart
|
||||
while (line < curDelta.source.position) {
|
||||
buffer.add(" " + origLines[line])
|
||||
origTotal++
|
||||
revTotal++
|
||||
line++
|
||||
}
|
||||
|
||||
// output the first Delta
|
||||
buffer.addAll(getDeltaText(curDelta))
|
||||
origTotal += curDelta.source.lines.size
|
||||
revTotal += curDelta.target.lines.size
|
||||
var deltaIndex = 1
|
||||
while (deltaIndex < deltas.size) { // for each of the other Deltas
|
||||
val nextDelta = deltas[deltaIndex]
|
||||
val intermediateStart = (curDelta.source.position + curDelta.source.lines.size)
|
||||
line = intermediateStart
|
||||
while (line < nextDelta.source.position) {
|
||||
// output the code between the last Delta and this one
|
||||
buffer.add(" " + origLines[line])
|
||||
origTotal++
|
||||
revTotal++
|
||||
line++
|
||||
}
|
||||
buffer.addAll(getDeltaText(nextDelta)) // output the Delta
|
||||
origTotal += nextDelta.source.lines.size
|
||||
revTotal += nextDelta.target.lines.size
|
||||
curDelta = nextDelta
|
||||
deltaIndex++
|
||||
}
|
||||
|
||||
// Now output the post-Delta context code, clamping the end of the file
|
||||
contextStart = (curDelta.source.position + curDelta.source.lines.size)
|
||||
line = contextStart
|
||||
while (line < contextStart + contextSize
|
||||
&& line < origLines.size
|
||||
) {
|
||||
buffer.add(" " + origLines[line])
|
||||
origTotal++
|
||||
revTotal++
|
||||
line++
|
||||
}
|
||||
|
||||
// Create and insert the block header, conforming to the Unified Diff standard
|
||||
buffer.add(0, "@@ -$origStart,$origTotal +$revStart,$revTotal @@")
|
||||
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 fun getDeltaText(delta: AbstractDelta<String>): List<String> {
|
||||
val buffer: MutableList<String> = ArrayList()
|
||||
for (line in delta.source.lines) {
|
||||
buffer.add("-$line")
|
||||
}
|
||||
for (line in delta.target.lines) {
|
||||
buffer.add("+$line")
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.algorithm
|
||||
|
||||
import dev.gitlive.difflib.patch.DeltaType
|
||||
|
||||
/**
|
||||
*
|
||||
* @author [Tobias Warneke](t.warneke@gmx.net)
|
||||
*/
|
||||
class Change(
|
||||
val deltaType: DeltaType,
|
||||
val startOriginal: Int,
|
||||
val endOriginal: Int,
|
||||
val startRevised: Int,
|
||||
val endRevised: Int
|
||||
) {
|
||||
fun withEndOriginal(endOriginal: Int): Change {
|
||||
return Change(deltaType, startOriginal, endOriginal, startRevised, endRevised)
|
||||
}
|
||||
|
||||
fun withEndRevised(endRevised: Int): Change {
|
||||
return Change(deltaType, startOriginal, endOriginal, startRevised, endRevised)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.algorithm
|
||||
|
||||
import dev.gitlive.difflib.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
|
||||
*/
|
||||
interface DiffAlgorithmFactory {
|
||||
fun <T> create(): DiffAlgorithmI<T>
|
||||
fun <T> create(equalizer: BiPredicate<T, T>): DiffAlgorithmI<T>
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.algorithm
|
||||
|
||||
/**
|
||||
* Interface of a diff algorithm.
|
||||
*
|
||||
* @author Tobias Warneke (t.warneke@gmx.net)
|
||||
* @param <T> type of data that is diffed.
|
||||
</T> */
|
||||
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
|
||||
*/
|
||||
fun computeDiff(source: List<T>, target: List<T>, progress: DiffAlgorithmListener?): List<Change>
|
||||
|
||||
/**
|
||||
* Simple extension to compute a changeset using arrays.
|
||||
*
|
||||
* @param source
|
||||
* @param target
|
||||
* @param progress
|
||||
* @return
|
||||
*/
|
||||
// @Throws(DiffException::class)
|
||||
fun computeDiff(source: Array<T>, target: Array<T>, progress: DiffAlgorithmListener?): List<Change> {
|
||||
return computeDiff(source.toList(), target.toList(), progress)
|
||||
}
|
||||
}
|
||||
@@ -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 dev.gitlive.difflib.algorithm
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Tobias Warneke (t.warneke@gmx.net)
|
||||
*/
|
||||
interface DiffAlgorithmListener {
|
||||
fun 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
|
||||
*/
|
||||
fun diffStep(value: Int, max: Int)
|
||||
fun diffEnd()
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.algorithm.myers
|
||||
|
||||
import dev.gitlive.difflib.BiPredicate
|
||||
import dev.gitlive.difflib.algorithm.Change
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmFactory
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmI
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmListener
|
||||
import dev.gitlive.difflib.patch.DeltaType
|
||||
|
||||
/**
|
||||
* A clean-room implementation of Eugene Meyers greedy differencing algorithm.
|
||||
*/
|
||||
class MeyersDiff<T> : DiffAlgorithmI<T> {
|
||||
private val equalizer: BiPredicate<T, T>
|
||||
|
||||
constructor() {
|
||||
equalizer = { a: T, b: T -> a!! == b }
|
||||
}
|
||||
|
||||
constructor(equalizer: BiPredicate<T, T>) {
|
||||
this.equalizer = equalizer
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Return empty diff if get the error while procession the difference.
|
||||
*/
|
||||
override fun computeDiff(source: List<T>, target: List<T>, progress: DiffAlgorithmListener?): List<Change> {
|
||||
progress?.diffStart()
|
||||
val path = buildPath(source, target, progress)
|
||||
val result = buildRevision(path, source, target)
|
||||
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 [Path][PathNode] accross the differences graph.
|
||||
* @throws DifferentiationFailedException if a diff path could not be found.
|
||||
*/
|
||||
private fun buildPath(orig: List<T>, rev: List<T>, progress: DiffAlgorithmListener?): PathNode {
|
||||
|
||||
// these are local constants
|
||||
val N = orig.size
|
||||
val M = rev.size
|
||||
val MAX = N + M + 1
|
||||
val size = 1 + 2 * MAX
|
||||
val middle = size / 2
|
||||
val diagonal = arrayOfNulls<PathNode>(size)
|
||||
diagonal[middle + 1] = PathNode(0, -1, true, true, null)
|
||||
for (d in 0 until MAX) {
|
||||
progress?.diffStep(d, MAX)
|
||||
var k = -d
|
||||
while (k <= d) {
|
||||
val kmiddle = middle + k
|
||||
val kplus = kmiddle + 1
|
||||
val kminus = kmiddle - 1
|
||||
var prev: PathNode?
|
||||
var i: Int
|
||||
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
|
||||
var j = i - k
|
||||
var node = PathNode(i, j, false, false, prev)
|
||||
while (i < N && j < M && equalizer(orig[i], rev[j])) {
|
||||
i++
|
||||
j++
|
||||
}
|
||||
if (i != node.i) {
|
||||
node = PathNode(i, j, true, false, node)
|
||||
}
|
||||
diagonal[kmiddle] = node
|
||||
if (i >= N && j >= M) {
|
||||
return diagonal[kmiddle]!!
|
||||
}
|
||||
k += 2
|
||||
}
|
||||
diagonal[middle + d - 1] = null
|
||||
}
|
||||
throw IllegalStateException("could not find a diff path")
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a [Patch] from a difference path.
|
||||
*
|
||||
* @param actualPath The path.
|
||||
* @param orig The original sequence.
|
||||
* @param rev The revised sequence.
|
||||
* @return A [Patch] script corresponding to the path.
|
||||
* @throws DifferentiationFailedException if a [Patch] could not be
|
||||
* built from the given path.
|
||||
*/
|
||||
private fun buildRevision(actualPath: PathNode, orig: List<T>, rev: List<T>): List<Change> {
|
||||
var path: PathNode? = actualPath
|
||||
val changes: MutableList<Change> = ArrayList()
|
||||
if (path!!.isSnake) {
|
||||
path = path.prev
|
||||
}
|
||||
while (path?.prev != null && path.prev!!.j >= 0) {
|
||||
check(!path.isSnake) { "bad diffpath: found snake when looking for diff" }
|
||||
val i = path.i
|
||||
val j = path.j
|
||||
path = path.prev
|
||||
val ianchor = path!!.i
|
||||
val janchor = path.j
|
||||
if (ianchor == i && janchor != j) {
|
||||
changes.add(Change(DeltaType.INSERT, ianchor, i, janchor, j))
|
||||
} else if (ianchor != i && janchor == j) {
|
||||
changes.add(Change(DeltaType.DELETE, ianchor, i, janchor, j))
|
||||
} else {
|
||||
changes.add(Change(DeltaType.CHANGE, ianchor, i, janchor, j))
|
||||
}
|
||||
if (path.isSnake) {
|
||||
path = path.prev
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Factory to create instances of this specific diff algorithm.
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun factory(): DiffAlgorithmFactory {
|
||||
return object : DiffAlgorithmFactory {
|
||||
override fun <T> create(): DiffAlgorithmI<T> {
|
||||
return MeyersDiff()
|
||||
}
|
||||
|
||||
override fun <T> create(equalizer: BiPredicate<T, T>): DiffAlgorithmI<T> {
|
||||
return MeyersDiff(equalizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.algorithm.myers
|
||||
|
||||
import dev.gitlive.difflib.BiPredicate
|
||||
import dev.gitlive.difflib.Consumer
|
||||
import dev.gitlive.difflib.algorithm.Change
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmFactory
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmI
|
||||
import dev.gitlive.difflib.algorithm.DiffAlgorithmListener
|
||||
import dev.gitlive.difflib.patch.DeltaType
|
||||
|
||||
/**
|
||||
*
|
||||
* @author tw
|
||||
*/
|
||||
class MeyersDiffWithLinearSpace<T> : DiffAlgorithmI<T> {
|
||||
private val equalizer: BiPredicate<T, T>
|
||||
|
||||
constructor() {
|
||||
equalizer = { a: T, b: T -> a!! == b }
|
||||
}
|
||||
|
||||
constructor(equalizer: BiPredicate<T, T>) {
|
||||
this.equalizer = equalizer
|
||||
}
|
||||
|
||||
override fun computeDiff(source: List<T>, target: List<T>, progress: DiffAlgorithmListener?): List<Change> {
|
||||
progress?.diffStart()
|
||||
val data = DiffData(source, target)
|
||||
val maxIdx = source.size + target.size
|
||||
buildScript(data, 0, source.size, 0, target.size) { idx: Int -> progress?.diffStep(idx, maxIdx) }
|
||||
progress?.diffEnd()
|
||||
return data.script
|
||||
}
|
||||
|
||||
private fun buildScript(data: DiffData, start1: Int, end1: Int, start2: Int, end2: Int, progress: Consumer<Int>?) {
|
||||
progress?.invoke((end1 - start1) / 2 + (end2 - start2) / 2)
|
||||
val middle: Snake? = getMiddleSnake(data, start1, end1, start2, end2)
|
||||
if (middle == null || middle.start == end1 && middle.diag == end1 - end2 || middle.end == start1 && middle.diag == start1 - start2) {
|
||||
var i = start1
|
||||
var j = start2
|
||||
while (i < end1 || j < end2) {
|
||||
if (i < end1 && j < end2 && equalizer(data.source[i], data.target[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[data.script.size - 1].endOriginal != i || data.script[data.script.size - 1].deltaType != DeltaType.DELETE
|
||||
) {
|
||||
data.script.add(Change(DeltaType.DELETE, i, i + 1, j, j))
|
||||
} else {
|
||||
data.script[data.script.size - 1] = data.script[data.script.size - 1].withEndOriginal(i + 1)
|
||||
}
|
||||
++i
|
||||
} else {
|
||||
if (data.script.isEmpty()
|
||||
|| data.script[data.script.size - 1].endRevised != j || data.script[data.script.size - 1].deltaType != DeltaType.INSERT
|
||||
) {
|
||||
data.script.add(Change(DeltaType.INSERT, i, i, j, j + 1))
|
||||
} else {
|
||||
data.script[data.script.size - 1] = data.script[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 fun getMiddleSnake(data: DiffData, start1: Int, end1: Int, start2: Int, end2: Int): Snake? {
|
||||
val m = end1 - start1
|
||||
val n = end2 - start2
|
||||
if (m == 0 || n == 0) {
|
||||
return null
|
||||
}
|
||||
val delta = m - n
|
||||
val sum = n + m
|
||||
val offset = (if (sum % 2 == 0) sum else sum + 1) / 2
|
||||
data.vDown[1 + offset] = start1
|
||||
data.vUp[1 + offset] = end1 + 1
|
||||
for (d in 0..offset) {
|
||||
// Down
|
||||
run {
|
||||
var k = -d
|
||||
while (k <= d) {
|
||||
|
||||
// First step
|
||||
val 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
|
||||
}
|
||||
var x = data.vDown[i]
|
||||
var y = x - start1 + start2 - k
|
||||
while (x < end1 && y < end2 && equalizer(data.source[x], data.target[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)
|
||||
}
|
||||
}
|
||||
k += 2
|
||||
}
|
||||
}
|
||||
|
||||
// Up
|
||||
var k = delta - d
|
||||
while (k <= delta + d) {
|
||||
|
||||
// First step
|
||||
val 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]
|
||||
}
|
||||
var x = data.vUp[i] - 1
|
||||
var y = x - start1 + start2 - k
|
||||
while (x >= start1 && y >= start2 && equalizer(data.source[x], data.target[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)
|
||||
}
|
||||
}
|
||||
k += 2
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("could not find a diff path")
|
||||
}
|
||||
|
||||
private fun buildSnake(data: DiffData, start: Int, diag: Int, end1: Int, end2: Int): Snake {
|
||||
var end = start
|
||||
while (end - diag < end2 && end < end1 && equalizer(data.source[end], data.target[end - diag])) {
|
||||
++end
|
||||
}
|
||||
return Snake(start, end, diag)
|
||||
}
|
||||
|
||||
private inner class DiffData(val source: List<T>, val target: List<T>) {
|
||||
val size: Int
|
||||
val vDown: IntArray
|
||||
val vUp: IntArray
|
||||
val script: MutableList<Change>
|
||||
|
||||
init {
|
||||
size = source.size + target.size + 2
|
||||
vDown = IntArray(size)
|
||||
vUp = IntArray(size)
|
||||
script = ArrayList()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Snake(val start: Int, val end: Int, val diag: Int)
|
||||
companion object {
|
||||
/**
|
||||
* Factory to create instances of this specific diff algorithm.
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun factory(): DiffAlgorithmFactory {
|
||||
return object : DiffAlgorithmFactory {
|
||||
override fun <T> create(): DiffAlgorithmI<T> {
|
||||
return MeyersDiffWithLinearSpace()
|
||||
}
|
||||
|
||||
override fun <T> create(equalizer: BiPredicate<T, T>): DiffAlgorithmI<T> {
|
||||
return MeyersDiffWithLinearSpace(equalizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.algorithm.myers
|
||||
|
||||
/**
|
||||
* A node in a diffpath.
|
||||
*
|
||||
* @author [Juanco Anez](mailto:juanco@suigeneris.org)
|
||||
*/
|
||||
class PathNode(
|
||||
/**
|
||||
* Position in the original sequence.
|
||||
*/
|
||||
val i: Int,
|
||||
/**
|
||||
* Position in the revised sequence.
|
||||
*/
|
||||
val j: Int, snake: Boolean,
|
||||
/**
|
||||
* Is this a bootstrap node?
|
||||
*
|
||||
*
|
||||
* In bottstrap nodes one of the two corrdinates is less than zero.
|
||||
*
|
||||
* @return tru if this is a bootstrap node.
|
||||
*/
|
||||
val isBootstrap: Boolean, prev: PathNode?
|
||||
) {
|
||||
/**
|
||||
* The previous node in the path.
|
||||
*/
|
||||
var prev: PathNode? = null
|
||||
val isSnake: Boolean
|
||||
|
||||
/**
|
||||
* Skips sequences of [PathNodes][PathNode] until a snake or bootstrap node is found, or the end of the
|
||||
* path is reached.
|
||||
*
|
||||
* @return The next first [PathNode] or bootstrap node in the path, or `null` if none found.
|
||||
*/
|
||||
fun previousSnake(): PathNode? {
|
||||
if (isBootstrap) {
|
||||
return null
|
||||
}
|
||||
return if (!isSnake && prev != null) {
|
||||
prev!!.previousSnake()
|
||||
} else this
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun toString(): String {
|
||||
val buf = StringBuilder("[")
|
||||
var node: PathNode? = 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
init {
|
||||
if (snake) {
|
||||
this.prev = prev
|
||||
} else {
|
||||
this.prev = prev?.previousSnake()
|
||||
}
|
||||
isSnake = snake
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* Abstract delta between a source and a target.
|
||||
* @author Tobias Warneke (t.warneke@gmx.net)
|
||||
*/
|
||||
abstract class AbstractDelta<T>(val type: DeltaType, val source: Chunk<T>, val target: Chunk<T>) {
|
||||
|
||||
/**
|
||||
* Verify the chunk of this delta, to fit the target.
|
||||
* @param target
|
||||
* @throws PatchFailedException
|
||||
*/
|
||||
// @Throws(PatchFailedException::class)
|
||||
protected fun verifyChunkToFitTarget(target: List<T>): VerifyChunk {
|
||||
return source.verifyChunk(target)
|
||||
}
|
||||
|
||||
// @Throws(PatchFailedException::class)
|
||||
fun verifyAntApplyTo(target: MutableList<T>): VerifyChunk {
|
||||
val verify = verifyChunkToFitTarget(target)
|
||||
if (verify == VerifyChunk.OK) {
|
||||
applyTo(target)
|
||||
}
|
||||
return verify
|
||||
}
|
||||
|
||||
// @Throws(PatchFailedException::class)
|
||||
protected abstract fun applyTo(target: MutableList<T>)
|
||||
abstract fun restore(target: MutableList<T>)
|
||||
|
||||
/**
|
||||
* Create a new delta of the actual instance with customized chunk data.
|
||||
*/
|
||||
abstract fun withChunks(original: Chunk<T>, revised: Chunk<T>): AbstractDelta<T>
|
||||
override fun hashCode(): Int {
|
||||
return Triple(this.source, this.target, this.type).hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
if (this::class != other::class) {
|
||||
return false
|
||||
}
|
||||
val other = other as AbstractDelta<*>
|
||||
if (source != other.source) {
|
||||
return false
|
||||
}
|
||||
return if (target != other.target) {
|
||||
false
|
||||
} else type == other.type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* Describes the change-delta between original and revised texts.
|
||||
*
|
||||
* @author [Dmitry Naumenko](dm.naumenko@gmail.com)
|
||||
* @param <T> The type of the compared elements in the data 'lines'.
|
||||
</T> */
|
||||
class ChangeDelta<T>(source: Chunk<T>, target: Chunk<T>) : AbstractDelta<T>(DeltaType.CHANGE, source, target) {
|
||||
// @Throws(PatchFailedException::class)
|
||||
override fun applyTo(target: MutableList<T>) {
|
||||
val position = source.position
|
||||
val size = source.size()
|
||||
for (i in 0 until size) {
|
||||
target.removeAt(position)
|
||||
}
|
||||
var i = 0
|
||||
for (line in this.target.lines) {
|
||||
target.add(position + i, line)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
override fun restore(target: MutableList<T>) {
|
||||
val position = this.target.position
|
||||
val size = this.target.size()
|
||||
for (i in 0 until size) {
|
||||
target.removeAt(position)
|
||||
}
|
||||
var i = 0
|
||||
for (line in source.lines) {
|
||||
target.add(position + i, line)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("[ChangeDelta, position: " + source.position + ", lines: "
|
||||
+ source.lines + " to " + target.lines + "]")
|
||||
}
|
||||
|
||||
override fun withChunks(original: Chunk<T>, revised: Chunk<T>): AbstractDelta<T> {
|
||||
return ChangeDelta(original, revised)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* Holds the information about the part of text involved in the diff process
|
||||
*
|
||||
*
|
||||
*
|
||||
* Text is represented as `Object[]` because the diff engine is
|
||||
* capable of handling more than plain ascci. In fact, arrays or lists of any
|
||||
* type that implements [hashCode()][java.lang.Object.hashCode] and
|
||||
* [equals()][java.lang.Object.equals] correctly can be subject to
|
||||
* differencing using this library.
|
||||
*
|
||||
*
|
||||
* @author [](dm.naumenko@gmail.com>Dmitry Naumenko</a>
|
||||
@param <T> The type of the compared elements in the 'lines'.
|
||||
) */
|
||||
class Chunk<T> {
|
||||
|
||||
/**
|
||||
* @return the start position of chunk in the text
|
||||
*/
|
||||
val position: Int
|
||||
|
||||
/**
|
||||
* @return the affected lines
|
||||
*/
|
||||
var lines: List<T>
|
||||
|
||||
/**
|
||||
* @return the positions of changed lines of chunk in the text
|
||||
*/
|
||||
val changePosition: List<Int>?
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
/**
|
||||
* Creates a chunk and saves a copy of affected lines
|
||||
*
|
||||
* @param position the start position
|
||||
* @param lines the affected lines
|
||||
*/
|
||||
// @JvmOverloads
|
||||
constructor(position: Int, lines: List<T>, changePosition: List<Int>? = null) {
|
||||
this.position = position
|
||||
this.lines = ArrayList(lines)
|
||||
this.changePosition = if (changePosition != null) ArrayList(changePosition) else 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
|
||||
*/
|
||||
/**
|
||||
* Creates a chunk and saves a copy of affected lines
|
||||
*
|
||||
* @param position the start position
|
||||
* @param lines the affected lines
|
||||
*/
|
||||
// @JvmOverloads
|
||||
constructor(position: Int, lines: Array<T>, changePosition: List<Int>? = null) {
|
||||
this.position = position
|
||||
this.lines = lines.toList()
|
||||
this.changePosition = if (changePosition != null) ArrayList(changePosition) else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that this chunk's saved text matches the corresponding text in
|
||||
* the given sequence.
|
||||
*
|
||||
* @param target the sequence to verify against.
|
||||
* @throws dev.gitlive.difflib.patch.PatchFailedException
|
||||
*/
|
||||
// @Throws(PatchFailedException::class)
|
||||
fun verifyChunk(target: List<T>): VerifyChunk {
|
||||
if (position > target.size || last() > target.size) {
|
||||
return VerifyChunk.POSITION_OUT_OF_TARGET
|
||||
}
|
||||
for (i in 0 until size()) {
|
||||
if (target[position + i] != lines[i]) {
|
||||
return VerifyChunk.CONTENT_DOES_NOT_MATCH_TARGET
|
||||
}
|
||||
}
|
||||
return VerifyChunk.OK
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
return lines.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the last line of the chunk.
|
||||
*/
|
||||
fun last(): Int {
|
||||
return position + size() - 1
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Triple(lines, position, size()).hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
if (this::class != other::class) {
|
||||
return false
|
||||
}
|
||||
val other = other as Chunk<*>?
|
||||
if (lines != other!!.lines) {
|
||||
return false
|
||||
}
|
||||
return position == other.position
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "[position: " + position + ", size: " + size() + ", lines: " + lines + "]"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*-
|
||||
* #%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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
*
|
||||
* @author tw
|
||||
*/
|
||||
interface ConflictOutput<T> {
|
||||
// @Throws(PatchFailedException::class)
|
||||
fun processConflict(verifyChunk: VerifyChunk?, delta: AbstractDelta<T>, result: MutableList<T>)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* Describes the delete-delta between original and revised texts.
|
||||
*
|
||||
* @author [Dmitry Naumenko](dm.naumenko@gmail.com)
|
||||
* @param <T> The type of the compared elements in the 'lines'.
|
||||
</T> */
|
||||
class DeleteDelta<T>
|
||||
/**
|
||||
* Creates a change delta with the two given chunks.
|
||||
*
|
||||
* @param original The original chunk. Must not be `null`.
|
||||
* @param revised The original chunk. Must not be `null`.
|
||||
*/
|
||||
(original: Chunk<T>, revised: Chunk<T>) : AbstractDelta<T>(DeltaType.DELETE, original, revised) {
|
||||
// @Throws(PatchFailedException::class)
|
||||
protected override fun applyTo(target: MutableList<T>) {
|
||||
val position = source.position
|
||||
val size = source.size()
|
||||
for (i in 0 until size) {
|
||||
target.removeAt(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun restore(target: MutableList<T>) {
|
||||
val position = this.target.position
|
||||
val lines: List<T> = source.lines
|
||||
for (i in lines.indices) {
|
||||
target.add(position + i, lines[i])
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("[DeleteDelta, position: " + source.position + ", lines: "
|
||||
+ source.lines + "]")
|
||||
}
|
||||
|
||||
override fun withChunks(original: Chunk<T>, revised: Chunk<T>): AbstractDelta<T> {
|
||||
return DeleteDelta(original, revised)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 dev.gitlive.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.
|
||||
*
|
||||
*/
|
||||
enum class DeltaType {
|
||||
/**
|
||||
* A change in the original.
|
||||
*/
|
||||
CHANGE,
|
||||
|
||||
/**
|
||||
* A delete from the original.
|
||||
*/
|
||||
DELETE,
|
||||
|
||||
/**
|
||||
* An insert into the original.
|
||||
*/
|
||||
INSERT,
|
||||
|
||||
/**
|
||||
* An do nothing.
|
||||
*/
|
||||
EQUAL
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* Base class for all exceptions emanating from this package.
|
||||
*
|
||||
* @author [Juanco Anez](mailto:juanco@suigeneris.org)
|
||||
*/
|
||||
open class DiffException : Exception {
|
||||
constructor() {}
|
||||
constructor(msg: String?) : super(msg) {}
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* This delta contains equal lines of data. Therefore nothing is to do in applyTo and restore.
|
||||
* @author tobens
|
||||
*/
|
||||
class EqualDelta<T>(source: Chunk<T>, target: Chunk<T>) : AbstractDelta<T>(DeltaType.EQUAL, source, target) {
|
||||
// @Throws(PatchFailedException::class)
|
||||
override fun applyTo(target: MutableList<T>) {
|
||||
}
|
||||
|
||||
override fun restore(target: MutableList<T>) {}
|
||||
override fun toString(): String {
|
||||
return ("[EqualDelta, position: " + source.position + ", lines: "
|
||||
+ source.lines + "]")
|
||||
}
|
||||
|
||||
override fun withChunks(original: Chunk<T>, revised: Chunk<T>): AbstractDelta<T> {
|
||||
return EqualDelta(original, revised)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* Describes the add-delta between original and revised texts.
|
||||
*
|
||||
* @author [Dmitry Naumenko](dm.naumenko@gmail.com)
|
||||
* @param <T> The type of the compared elements in the 'lines'.
|
||||
</T> */
|
||||
class InsertDelta<T>
|
||||
/**
|
||||
* Creates an insert delta with the two given chunks.
|
||||
*
|
||||
* @param original The original chunk. Must not be `null`.
|
||||
* @param revised The original chunk. Must not be `null`.
|
||||
*/
|
||||
(original: Chunk<T>, revised: Chunk<T>) : AbstractDelta<T>(DeltaType.INSERT, original, revised) {
|
||||
// @Throws(PatchFailedException::class)
|
||||
protected override fun applyTo(target: MutableList<T>) {
|
||||
val position = source.position
|
||||
val lines: List<T> = this.target.lines
|
||||
for (i in lines.indices) {
|
||||
target.add(position + i, lines[i])
|
||||
}
|
||||
}
|
||||
|
||||
override fun restore(target: MutableList<T>) {
|
||||
val position = this.target.position
|
||||
val size = this.target.size()
|
||||
for (i in 0 until size) {
|
||||
target.removeAt(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("[InsertDelta, position: " + source.position
|
||||
+ ", lines: " + target.lines + "]")
|
||||
}
|
||||
|
||||
override fun withChunks(original: Chunk<T>, revised: Chunk<T>): AbstractDelta<T> {
|
||||
return InsertDelta(original, revised)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*-
|
||||
* #%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 dev.gitlive.difflib.patch
|
||||
|
||||
import dev.gitlive.difflib.algorithm.Change
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
/**
|
||||
* Describes the patch holding all deltas between the original and revised
|
||||
* texts.
|
||||
*
|
||||
* @author [Dmitry Naumenko](dm.naumenko@gmail.com)
|
||||
* @param <T> The type of the compared elements in the 'lines'.
|
||||
</T> */
|
||||
class Patch<T> @JvmOverloads constructor(estimatedPatchSize: Int = 10) {
|
||||
private val deltas: MutableList<AbstractDelta<T>>
|
||||
|
||||
/**
|
||||
* Apply this patch to the given target
|
||||
*
|
||||
* @return the patched text
|
||||
* @throws PatchFailedException if can't apply patch
|
||||
*/
|
||||
// @Throws(PatchFailedException::class)
|
||||
fun applyTo(target: List<T>): List<T> {
|
||||
val result: MutableList<T> = ArrayList(target)
|
||||
val it = getDeltas().listIterator(deltas.size)
|
||||
while (it.hasPrevious()) {
|
||||
val delta = it.previous()
|
||||
val valid = delta.verifyAntApplyTo(result)
|
||||
if (valid != VerifyChunk.OK) {
|
||||
conflictOutput.processConflict(valid, delta, result)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Patch behaviour to throw an exception for pathching conflicts.
|
||||
*/
|
||||
val CONFLICT_PRODUCES_EXCEPTION =
|
||||
object : ConflictOutput<T> {
|
||||
override fun processConflict(verifyChunk: VerifyChunk?, delta: AbstractDelta<T>, result: MutableList<T>) {
|
||||
throw PatchFailedException(
|
||||
"could not apply patch due to $verifyChunk"
|
||||
)
|
||||
}
|
||||
}
|
||||
private var conflictOutput = CONFLICT_PRODUCES_EXCEPTION
|
||||
|
||||
/**
|
||||
* Alter normal conflict output behaviour to e.g. inclide some conflict
|
||||
* statements in the result, like git does it.
|
||||
*/
|
||||
fun withConflictOutput(conflictOutput: ConflictOutput<T>): Patch<*> {
|
||||
this.conflictOutput = conflictOutput
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the text to original. Opposite to applyTo() method.
|
||||
*
|
||||
* @param target the given target
|
||||
* @return the restored text
|
||||
*/
|
||||
fun restore(target: List<T>): List<T> {
|
||||
val result: MutableList<T> = ArrayList(target)
|
||||
val it = getDeltas().listIterator(deltas.size)
|
||||
while (it.hasPrevious()) {
|
||||
val delta = it.previous()
|
||||
delta.restore(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given delta to this patch
|
||||
*
|
||||
* @param delta the given delta
|
||||
*/
|
||||
fun addDelta(delta: AbstractDelta<T>) {
|
||||
deltas.add(delta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of computed deltas
|
||||
*
|
||||
* @return the deltas
|
||||
*/
|
||||
fun getDeltas(): MutableList<AbstractDelta<T>> {
|
||||
deltas.sortBy { d -> d.source.position }
|
||||
return deltas
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Patch{deltas=$deltas}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Git like merge conflict output.
|
||||
*/
|
||||
@kotlin.jvm.JvmField
|
||||
val CONFLICT_PRODUCES_MERGE_CONFLICT =
|
||||
object : ConflictOutput<String> {
|
||||
override fun processConflict(verifyChunk: VerifyChunk?, delta: AbstractDelta<String>, result: MutableList<String>) {
|
||||
if (result.size > delta.source.position) {
|
||||
val orgData: MutableList<String> = ArrayList()
|
||||
for (i in 0 until delta.source.size()) {
|
||||
orgData.add(result[delta.source.position])
|
||||
result.removeAt(delta.source.position)
|
||||
}
|
||||
orgData.add(0, "<<<<<< HEAD")
|
||||
orgData.add("======")
|
||||
orgData.addAll(delta.source.lines)
|
||||
orgData.add(">>>>>>> PATCH")
|
||||
result.addAll(delta.source.position, orgData)
|
||||
} else {
|
||||
throw UnsupportedOperationException("Not supported yet.") //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> generate(original: List<T>, revised: List<T>, changes: List<Change>): Patch<T> {
|
||||
return generate(original, revised, changes, false)
|
||||
}
|
||||
|
||||
private fun <T> buildChunk(start: Int, end: Int, data: List<T>): Chunk<T> {
|
||||
return Chunk(start, ArrayList(data.subList(start, end)))
|
||||
}
|
||||
|
||||
fun <T> generate(
|
||||
original: List<T>,
|
||||
revised: List<T>,
|
||||
_changes: List<Change>,
|
||||
includeEquals: Boolean
|
||||
): Patch<T> {
|
||||
val patch = Patch<T>(_changes.size)
|
||||
var startOriginal = 0
|
||||
var startRevised = 0
|
||||
var changes = _changes
|
||||
if (includeEquals) {
|
||||
changes = ArrayList(_changes)
|
||||
changes.sortBy { d -> d.startOriginal }
|
||||
}
|
||||
for (change in changes) {
|
||||
if (includeEquals && startOriginal < change.startOriginal) {
|
||||
patch.addDelta(
|
||||
EqualDelta(
|
||||
buildChunk(startOriginal, change.startOriginal, original),
|
||||
buildChunk(startRevised, change.startRevised, revised)
|
||||
)
|
||||
)
|
||||
}
|
||||
val orgChunk = buildChunk(change.startOriginal, change.endOriginal, original)
|
||||
val revChunk = buildChunk(change.startRevised, change.endRevised, revised)
|
||||
when (change.deltaType) {
|
||||
DeltaType.DELETE -> patch.addDelta(DeleteDelta(orgChunk, revChunk))
|
||||
DeltaType.INSERT -> patch.addDelta(InsertDelta(orgChunk, revChunk))
|
||||
DeltaType.CHANGE -> patch.addDelta(ChangeDelta(orgChunk, revChunk))
|
||||
else -> {}
|
||||
}
|
||||
startOriginal = change.endOriginal
|
||||
startRevised = change.endRevised
|
||||
}
|
||||
if (includeEquals && startOriginal < original.size) {
|
||||
patch.addDelta(
|
||||
EqualDelta(
|
||||
buildChunk(startOriginal, original.size, original),
|
||||
buildChunk(startRevised, revised.size, revised)
|
||||
)
|
||||
)
|
||||
}
|
||||
return patch
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
deltas = ArrayList(estimatedPatchSize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
* Thrown whenever a delta cannot be applied as a patch to a given text.
|
||||
*
|
||||
* @author [Juanco Anez](mailto:juanco@suigeneris.org)
|
||||
*/
|
||||
class PatchFailedException : DiffException {
|
||||
constructor() {}
|
||||
constructor(msg: String?) : super(msg) {}
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.patch
|
||||
|
||||
/**
|
||||
*
|
||||
* @author tw
|
||||
*/
|
||||
enum class VerifyChunk {
|
||||
OK, POSITION_OUT_OF_TARGET, CONTENT_DOES_NOT_MATCH_TARGET
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.text
|
||||
|
||||
/**
|
||||
* Describes the diff row in form [tag, oldLine, newLine) for showing the difference between two texts
|
||||
*
|
||||
* @author [Dmitry Naumenko](dm.naumenko@gmail.com)
|
||||
*/
|
||||
class DiffRow(
|
||||
/**
|
||||
* @param tag the tag to set
|
||||
*/
|
||||
var tag: Tag,
|
||||
/**
|
||||
* @return the oldLine
|
||||
*/
|
||||
val oldLine: String,
|
||||
/**
|
||||
* @return the newLine
|
||||
*/
|
||||
val newLine: String
|
||||
) {
|
||||
/**
|
||||
* @return the tag
|
||||
*/
|
||||
|
||||
enum class Tag {
|
||||
INSERT, DELETE, CHANGE, EQUAL
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Triple(newLine, oldLine, tag).hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
if (this::class != other::class) {
|
||||
return false
|
||||
}
|
||||
val other = other as DiffRow
|
||||
if (newLine != other.newLine) {
|
||||
return false
|
||||
}
|
||||
if (oldLine != other.oldLine) {
|
||||
return false
|
||||
}
|
||||
if (tag != other.tag) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "[" + tag + "," + oldLine + "," + newLine + "]"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.text
|
||||
|
||||
import dev.gitlive.difflib.BiFunction
|
||||
import dev.gitlive.difflib.BiPredicate
|
||||
import dev.gitlive.difflib.DiffUtils
|
||||
import dev.gitlive.difflib.Function
|
||||
import dev.gitlive.difflib.patch.*
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* This class for generating DiffRows for side-by-sidy view. You can customize
|
||||
* the way of generating. For example, show inline diffs on not, ignoring white
|
||||
* spaces or/and blank lines and so on. All parameters for generating are
|
||||
* optional. If you do not specify them, the class will use the default values.
|
||||
*
|
||||
* These values are: showInlineDiffs = false; ignoreWhiteSpaces = true;
|
||||
* ignoreBlankLines = true; ...
|
||||
*
|
||||
* For instantiating the DiffRowGenerator you should use the its builder. Like
|
||||
* in example `
|
||||
* DiffRowGenerator generator = new DiffRowGenerator.Builder().showInlineDiffs(true).
|
||||
* ignoreWhiteSpaces(true).columnWidth(100).build();
|
||||
` *
|
||||
*/
|
||||
class DiffRowGenerator private constructor(builder: Builder) {
|
||||
private val columnWidth: Int
|
||||
private var equalizer: BiPredicate<String, String>? = null
|
||||
private val ignoreWhiteSpaces: Boolean
|
||||
private val inlineDiffSplitter: Function<String, MutableList<String>>
|
||||
private val mergeOriginalRevised: Boolean
|
||||
private val newTag: BiFunction<DiffRow.Tag, Boolean, String>
|
||||
private val oldTag: BiFunction<DiffRow.Tag, Boolean, String>
|
||||
private val reportLinesUnchanged: Boolean
|
||||
private val lineNormalizer: Function<String, String>
|
||||
private val processDiffs: Function<String, String>?
|
||||
private val showInlineDiffs: Boolean
|
||||
private val replaceOriginalLinefeedInChangesWithSpaces: Boolean
|
||||
private val decompressDeltas: Boolean
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun generateDiffRows(original: List<String>, revised: List<String>): List<DiffRow> {
|
||||
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
|
||||
*/
|
||||
fun generateDiffRows(original: List<String>, patch: Patch<String>): List<DiffRow> {
|
||||
val diffRows: MutableList<DiffRow> = ArrayList()
|
||||
var endPos = 0
|
||||
val deltaList: List<AbstractDelta<String>> = patch.getDeltas()
|
||||
if (decompressDeltas) {
|
||||
for (originalDelta in deltaList) {
|
||||
for (delta in decompressDeltas(originalDelta)) {
|
||||
endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (delta in deltaList) {
|
||||
endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the final matching chunk if any.
|
||||
for (line in original.subList(endPos, original.size)) {
|
||||
diffRows.add(buildDiffRow(DiffRow.Tag.EQUAL, line, line))
|
||||
}
|
||||
return diffRows
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms one patch delta into a DiffRow object.
|
||||
*/
|
||||
private fun transformDeltaIntoDiffRow(
|
||||
original: List<String>,
|
||||
endPos: Int,
|
||||
diffRows: MutableList<DiffRow>,
|
||||
delta: AbstractDelta<String>
|
||||
): Int {
|
||||
val orig: Chunk<String> = delta.source
|
||||
val rev: Chunk<String> = delta.target
|
||||
for (line in original.subList(endPos, orig.position)) {
|
||||
diffRows.add(buildDiffRow(DiffRow.Tag.EQUAL, line, line))
|
||||
}
|
||||
when (delta.type) {
|
||||
DeltaType.INSERT -> for (line in rev.lines) {
|
||||
diffRows.add(buildDiffRow(DiffRow.Tag.INSERT, "", line))
|
||||
}
|
||||
DeltaType.DELETE -> for (line in orig.lines) {
|
||||
diffRows.add(buildDiffRow(DiffRow.Tag.DELETE, line, ""))
|
||||
}
|
||||
else -> if (showInlineDiffs) {
|
||||
diffRows.addAll(generateInlineDiffs(delta))
|
||||
} else {
|
||||
var j = 0
|
||||
while (j < max(orig.size(), rev.size())) {
|
||||
diffRows.add(
|
||||
buildDiffRow(
|
||||
DiffRow.Tag.CHANGE,
|
||||
if (orig.lines.size > j) orig.lines[j] else "",
|
||||
if (rev.lines.size > j) rev.lines[j] else ""
|
||||
)
|
||||
)
|
||||
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 delta
|
||||
*/
|
||||
private fun decompressDeltas(delta: AbstractDelta<String>): List<AbstractDelta<String>> {
|
||||
if (delta.type == DeltaType.CHANGE && delta.source.size() != delta.target.size()) {
|
||||
val deltas: MutableList<AbstractDelta<String>> = ArrayList()
|
||||
//System.out.println("decompress this " + delta);
|
||||
val minSize = delta.source.size().coerceAtMost(delta.target.size())
|
||||
val orig: Chunk<String> = delta.source
|
||||
val rev: Chunk<String> = delta.target
|
||||
deltas.add(
|
||||
ChangeDelta(
|
||||
Chunk(orig.position, orig.lines.subList(0, minSize)),
|
||||
Chunk(rev.position, rev.lines.subList(0, minSize))
|
||||
)
|
||||
)
|
||||
if (orig.lines.size < rev.lines.size) {
|
||||
deltas.add(
|
||||
InsertDelta(
|
||||
Chunk(orig.position + minSize, emptyList()),
|
||||
Chunk(rev.position + minSize, rev.lines.subList(minSize, rev.lines.size))
|
||||
)
|
||||
)
|
||||
} else {
|
||||
deltas.add(
|
||||
DeleteDelta(
|
||||
Chunk(orig.position + minSize, orig.lines.subList(minSize, orig.lines.size)),
|
||||
Chunk(rev.position + minSize, emptyList())
|
||||
)
|
||||
)
|
||||
}
|
||||
return deltas
|
||||
}
|
||||
return listOf(delta)
|
||||
}
|
||||
|
||||
private fun buildDiffRow(type: DiffRow.Tag, orgline: String, newline: String): DiffRow {
|
||||
return if (reportLinesUnchanged) {
|
||||
DiffRow(type, orgline, newline)
|
||||
} else {
|
||||
var wrapOrg = preprocessLine(orgline)
|
||||
if (DiffRow.Tag.DELETE == type) {
|
||||
if (mergeOriginalRevised || showInlineDiffs) {
|
||||
wrapOrg = oldTag(type, true) + wrapOrg + oldTag(type, false)
|
||||
}
|
||||
}
|
||||
var wrapNew = preprocessLine(newline)
|
||||
if (DiffRow.Tag.INSERT == type) {
|
||||
if (mergeOriginalRevised) {
|
||||
wrapOrg = newTag(type, true) + wrapNew + newTag(type, false)
|
||||
} else if (showInlineDiffs) {
|
||||
wrapNew = newTag(type, true) + wrapNew + newTag(type, false)
|
||||
}
|
||||
}
|
||||
DiffRow(type, wrapOrg, wrapNew)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDiffRowWithoutNormalizing(type: DiffRow.Tag, orgline: String, newline: String): DiffRow {
|
||||
return DiffRow(
|
||||
type,
|
||||
StringUtils.wrapText(orgline, columnWidth),
|
||||
StringUtils.wrapText(newline, columnWidth)
|
||||
)
|
||||
}
|
||||
|
||||
fun normalizeLines(list: List<String>): List<String> {
|
||||
return if (reportLinesUnchanged) list else list.asSequence()
|
||||
.map { t: String -> lineNormalizer(t) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the inline diffs for given delta
|
||||
*
|
||||
* @param delta the given delta
|
||||
*/
|
||||
private fun generateInlineDiffs(delta: AbstractDelta<String>): List<DiffRow> {
|
||||
val orig = normalizeLines(delta.source.lines)
|
||||
val rev = normalizeLines(delta.target.lines)
|
||||
val origList: MutableList<String>
|
||||
val revList: MutableList<String>
|
||||
val joinedOrig = orig.joinToString("\n")
|
||||
val joinedRev = rev.joinToString("\n")
|
||||
origList = inlineDiffSplitter(joinedOrig)
|
||||
revList = inlineDiffSplitter(joinedRev)
|
||||
val inlineDeltas: MutableList<AbstractDelta<String>> = DiffUtils.diff(origList, revList, equalizer).getDeltas()
|
||||
inlineDeltas.reverse()
|
||||
for (inlineDelta in inlineDeltas) {
|
||||
val inlineOrig: Chunk<String> = inlineDelta.source
|
||||
val inlineRev: Chunk<String> = inlineDelta.target
|
||||
if (inlineDelta.type == DeltaType.DELETE) {
|
||||
wrapInTag(
|
||||
origList,
|
||||
inlineOrig.position,
|
||||
inlineOrig
|
||||
.position
|
||||
+ inlineOrig.size(),
|
||||
DiffRow.Tag.DELETE,
|
||||
oldTag,
|
||||
processDiffs,
|
||||
replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised
|
||||
)
|
||||
} else if (inlineDelta.type == DeltaType.INSERT) {
|
||||
if (mergeOriginalRevised) {
|
||||
origList.addAll(
|
||||
inlineOrig.position,
|
||||
revList.subList(
|
||||
inlineRev.position,
|
||||
inlineRev.position + inlineRev.size()
|
||||
)
|
||||
)
|
||||
wrapInTag(
|
||||
origList, inlineOrig.position,
|
||||
inlineOrig.position + inlineRev.size(),
|
||||
DiffRow.Tag.INSERT, newTag, processDiffs, false
|
||||
)
|
||||
} else {
|
||||
wrapInTag(
|
||||
revList, inlineRev.position,
|
||||
inlineRev.position + inlineRev.size(),
|
||||
DiffRow.Tag.INSERT, newTag, processDiffs, false
|
||||
)
|
||||
}
|
||||
} else if (inlineDelta.type == DeltaType.CHANGE) {
|
||||
if (mergeOriginalRevised) {
|
||||
origList.addAll(
|
||||
inlineOrig.position + inlineOrig.size(),
|
||||
revList.subList(
|
||||
inlineRev.position,
|
||||
inlineRev.position + inlineRev.size()
|
||||
)
|
||||
)
|
||||
wrapInTag(
|
||||
origList, inlineOrig.position + inlineOrig.size(),
|
||||
inlineOrig.position + inlineOrig.size() + inlineRev.size(),
|
||||
DiffRow.Tag.CHANGE, newTag, processDiffs, false
|
||||
)
|
||||
} else {
|
||||
wrapInTag(
|
||||
revList, inlineRev.position,
|
||||
inlineRev.position + inlineRev.size(),
|
||||
DiffRow.Tag.CHANGE, newTag, processDiffs, false
|
||||
)
|
||||
}
|
||||
wrapInTag(
|
||||
origList,
|
||||
inlineOrig.position,
|
||||
inlineOrig.position + inlineOrig.size(),
|
||||
DiffRow.Tag.CHANGE,
|
||||
oldTag,
|
||||
processDiffs,
|
||||
replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised
|
||||
)
|
||||
}
|
||||
}
|
||||
val origResult = StringBuilder()
|
||||
val revResult = StringBuilder()
|
||||
for (character in origList) {
|
||||
origResult.append(character)
|
||||
}
|
||||
for (character in revList) {
|
||||
revResult.append(character)
|
||||
}
|
||||
val original = origResult.toString().trimEnd('\n').lines()
|
||||
val revised = revResult.toString().trimEnd('\n').lines()
|
||||
val diffRows: MutableList<DiffRow> = ArrayList()
|
||||
for (j in 0 until max(original.size, revised.size)) {
|
||||
diffRows.add(
|
||||
buildDiffRowWithoutNormalizing(
|
||||
DiffRow.Tag.CHANGE,
|
||||
if (original.size > j) original[j] else "",
|
||||
if (revised.size > j) revised[j] else ""
|
||||
)
|
||||
)
|
||||
}
|
||||
return diffRows
|
||||
}
|
||||
|
||||
private fun preprocessLine(line: String): String {
|
||||
return if (columnWidth == 0) {
|
||||
lineNormalizer(line)
|
||||
} else {
|
||||
StringUtils.wrapText(lineNormalizer(line), columnWidth)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class used for building the DiffRowGenerator.
|
||||
*
|
||||
* @author dmitry
|
||||
*/
|
||||
class Builder {
|
||||
var showInlineDiffs = false
|
||||
var ignoreWhiteSpaces = false
|
||||
var decompressDeltas = true
|
||||
var oldTag =
|
||||
{ _: DiffRow.Tag, f: Boolean -> if (f) "<span class=\"editOldInline\">" else "</span>" }
|
||||
var newTag =
|
||||
{ _: DiffRow.Tag, f: Boolean -> if (f) "<span class=\"editNewInline\">" else "</span>" }
|
||||
var columnWidth = 0
|
||||
var mergeOriginalRevised = false
|
||||
var reportLinesUnchanged = false
|
||||
var inlineDiffSplitter = SPLITTER_BY_CHARACTER
|
||||
var lineNormalizer = LINE_NORMALIZER_FOR_HTML
|
||||
var processDiffs: Function<String, String>? = null
|
||||
var equalizer: BiPredicate<String, String>? = null
|
||||
var replaceOriginalLinefeedInChangesWithSpaces = false
|
||||
|
||||
/**
|
||||
* Show inline diffs in generating diff rows or not.
|
||||
*
|
||||
* @param val the value to set. Default: false.
|
||||
* @return builder with configured showInlineDiff parameter
|
||||
*/
|
||||
fun showInlineDiffs(`val`: Boolean): Builder {
|
||||
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
|
||||
*/
|
||||
fun ignoreWhiteSpaces(`val`: Boolean): Builder {
|
||||
ignoreWhiteSpaces = `val`
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the originial old and new text lines to Diffrow without any
|
||||
* additional processing and without any tags to highlight the change.
|
||||
*
|
||||
* @param val the value to set. Default: false.
|
||||
* @return builder with configured reportLinesUnWrapped parameter
|
||||
*/
|
||||
fun reportLinesUnchanged(`val`: Boolean): Builder {
|
||||
reportLinesUnchanged = `val`
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator for Old-Text-Tags.
|
||||
*
|
||||
* @param generator the tag generator
|
||||
* @return builder with configured ignoreBlankLines parameter
|
||||
*/
|
||||
fun oldTag(generator: BiFunction<DiffRow.Tag, Boolean, String>): Builder {
|
||||
oldTag = generator
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator for Old-Text-Tags.
|
||||
*
|
||||
* @param generator the tag generator
|
||||
* @return builder with configured ignoreBlankLines parameter
|
||||
*/
|
||||
fun oldTag(generator: Function<Boolean?, String>): Builder {
|
||||
oldTag = { _: DiffRow.Tag?, f: Boolean? -> generator(f) }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator for New-Text-Tags.
|
||||
*
|
||||
* @param generator
|
||||
* @return
|
||||
*/
|
||||
fun newTag(generator: BiFunction<DiffRow.Tag, Boolean, String>): Builder {
|
||||
newTag = generator
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator for New-Text-Tags.
|
||||
*
|
||||
* @param generator
|
||||
* @return
|
||||
*/
|
||||
fun newTag(generator: Function<Boolean?, String>): Builder {
|
||||
newTag = { _: DiffRow.Tag?, f: Boolean? -> generator(f) }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Processor for diffed text parts. Here e.g. whitecharacters could be
|
||||
* replaced by something visible.
|
||||
*
|
||||
* @param processDiffs
|
||||
* @return
|
||||
*/
|
||||
fun processDiffs(processDiffs: Function<String, String>?): Builder {
|
||||
this.processDiffs = processDiffs
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the column width of generated lines of original and revised
|
||||
* texts.
|
||||
*
|
||||
* @param width the width to set. Making it < 0 doesn't make any
|
||||
* sense. Default 80.
|
||||
* @return builder with config of column width
|
||||
*/
|
||||
fun columnWidth(width: Int): Builder {
|
||||
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
|
||||
*/
|
||||
fun build(): DiffRowGenerator {
|
||||
return DiffRowGenerator(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the complete result within the original text. This makes sense
|
||||
* for one line display.
|
||||
*
|
||||
* @param mergeOriginalRevised
|
||||
* @return
|
||||
*/
|
||||
fun mergeOriginalRevised(mergeOriginalRevised: Boolean): Builder {
|
||||
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
|
||||
*/
|
||||
fun decompressDeltas(decompressDeltas: Boolean): Builder {
|
||||
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> *
|
||||
*/
|
||||
fun inlineDiffByWord(inlineDiffByWord: Boolean): Builder {
|
||||
inlineDiffSplitter = if (inlineDiffByWord) SPLITTER_BY_WORD else 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
|
||||
*/
|
||||
fun inlineDiffBySplitter(inlineDiffSplitter: Function<String, MutableList<String>>): Builder {
|
||||
this.inlineDiffSplitter = inlineDiffSplitter
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* By default DiffRowGenerator preprocesses lines for HTML output. Tabs
|
||||
* and special HTML characters like "<" are replaced with its encoded
|
||||
* value. To change this you can provide a customized line normalizer
|
||||
* here.
|
||||
*
|
||||
* @param lineNormalizer
|
||||
* @return
|
||||
*/
|
||||
fun lineNormalizer(lineNormalizer: Function<String, String>): Builder {
|
||||
this.lineNormalizer = lineNormalizer
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an equalizer for diff processing.
|
||||
*
|
||||
* @param equalizer equalizer for diff processing.
|
||||
* @return builder with configured equalizer parameter
|
||||
*/
|
||||
fun equalizer(equalizer: BiPredicate<String, String>?): Builder {
|
||||
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
|
||||
*/
|
||||
fun replaceOriginalLinefeedInChangesWithSpaces(replace: Boolean): Builder {
|
||||
replaceOriginalLinefeedInChangesWithSpaces = replace
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DEFAULT_EQUALIZER = { obj1: String, obj2: String -> obj1 == obj2 }
|
||||
val IGNORE_WHITESPACE_EQUALIZER = { original: String, revised: String -> adjustWhitespace(original) == adjustWhitespace(revised) }
|
||||
val LINE_NORMALIZER_FOR_HTML = { obj: String -> StringUtils.normalize(obj) }
|
||||
|
||||
/**
|
||||
* Splitting lines by character to achieve char by char diff checking.
|
||||
*/
|
||||
val SPLITTER_BY_CHARACTER = { line: String ->
|
||||
val list: MutableList<String> = ArrayList<String>(line.length)
|
||||
for (character in line) {
|
||||
list.add(character.toString())
|
||||
}
|
||||
list
|
||||
}
|
||||
@kotlin.jvm.JvmField
|
||||
val SPLIT_BY_WORD_PATTERN = Regex("\\s+|[,.\\[\\](){}/\\\\*+\\-#]")
|
||||
|
||||
/**
|
||||
* Splitting lines by word to achieve word by word diff checking.
|
||||
*/
|
||||
val SPLITTER_BY_WORD = { line: String ->
|
||||
splitStringPreserveDelimiter(
|
||||
line,
|
||||
SPLIT_BY_WORD_PATTERN
|
||||
)
|
||||
}
|
||||
val WHITESPACE_PATTERN = Regex("\\s+")
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun create(): Builder {
|
||||
return Builder()
|
||||
}
|
||||
|
||||
private fun adjustWhitespace(raw: String): String {
|
||||
return WHITESPACE_PATTERN.replace(raw.trim { it <= ' ' }, " ")
|
||||
}
|
||||
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun splitStringPreserveDelimiter(str: String?, SPLIT_PATTERN: Regex): MutableList<String> {
|
||||
val list = mutableListOf<String>()
|
||||
if (str != null) {
|
||||
val results = SPLIT_PATTERN.findAll(str)
|
||||
var pos = 0
|
||||
for (result in results) {
|
||||
if (pos < result.range.first) {
|
||||
list.add(str.substring(pos, result.range.first))
|
||||
}
|
||||
list.add(result.value)
|
||||
pos = result.range.last + 1
|
||||
}
|
||||
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
|
||||
*/
|
||||
fun wrapInTag(
|
||||
sequence: MutableList<String>, startPosition: Int,
|
||||
endPosition: Int, tag: DiffRow.Tag, tagGenerator: BiFunction<DiffRow.Tag, Boolean, String>,
|
||||
processDiffs: Function<String, String>?, replaceLinefeedWithSpace: Boolean
|
||||
) {
|
||||
var endPos = endPosition
|
||||
while (endPos >= startPosition) {
|
||||
|
||||
//search position for end tag
|
||||
while (endPos > startPosition) {
|
||||
if ("\n" != sequence[endPos - 1]) {
|
||||
break
|
||||
} else if (replaceLinefeedWithSpace) {
|
||||
sequence[endPos - 1] = " "
|
||||
break
|
||||
}
|
||||
endPos--
|
||||
}
|
||||
if (endPos == startPosition) {
|
||||
break
|
||||
}
|
||||
sequence.add(endPos, tagGenerator(tag, false))
|
||||
if (processDiffs != null) {
|
||||
sequence[endPos - 1] = processDiffs(sequence[endPos - 1])
|
||||
}
|
||||
endPos--
|
||||
|
||||
//search position for end tag
|
||||
while (endPos > startPosition) {
|
||||
if ("\n" == sequence[endPos - 1]) {
|
||||
if (replaceLinefeedWithSpace) {
|
||||
sequence[endPos - 1] = " "
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (processDiffs != null) {
|
||||
sequence[endPos - 1] = processDiffs(sequence[endPos - 1])
|
||||
}
|
||||
endPos--
|
||||
}
|
||||
sequence.add(endPos, tagGenerator(tag, true))
|
||||
endPos--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
showInlineDiffs = builder.showInlineDiffs
|
||||
ignoreWhiteSpaces = builder.ignoreWhiteSpaces
|
||||
oldTag = builder.oldTag
|
||||
newTag = builder.newTag
|
||||
columnWidth = builder.columnWidth
|
||||
mergeOriginalRevised = builder.mergeOriginalRevised
|
||||
inlineDiffSplitter = builder.inlineDiffSplitter
|
||||
decompressDeltas = builder.decompressDeltas
|
||||
equalizer = if (builder.equalizer != null) {
|
||||
builder.equalizer
|
||||
} else {
|
||||
if (ignoreWhiteSpaces) IGNORE_WHITESPACE_EQUALIZER else DEFAULT_EQUALIZER
|
||||
}
|
||||
reportLinesUnchanged = builder.reportLinesUnchanged
|
||||
lineNormalizer = builder.lineNormalizer
|
||||
processDiffs = builder.processDiffs
|
||||
replaceOriginalLinefeedInChangesWithSpaces = builder.replaceOriginalLinefeedInChangesWithSpaces
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.text
|
||||
|
||||
internal object StringUtils {
|
||||
/**
|
||||
* Replaces all opening and closing tags with `<` or `>`.
|
||||
*
|
||||
* @param str
|
||||
* @return str with some HTML meta characters escaped.
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun htmlEntites(str: String): String {
|
||||
return str.replace("<", "<").replace(">", ">")
|
||||
}
|
||||
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun normalize(str: String): String {
|
||||
return htmlEntites(str).replace("\t", " ")
|
||||
}
|
||||
|
||||
fun wrapText(list: List<String>, columnWidth: Int): List<String> {
|
||||
return list.asSequence()
|
||||
.map { line: String -> wrapText(line, columnWidth) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the text with the given column width
|
||||
*
|
||||
* @param line the text
|
||||
* @param columnWidth the given column
|
||||
* @return the wrapped text
|
||||
*/
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun wrapText(line: String, columnWidth: Int): String {
|
||||
require(columnWidth >= 0) { "columnWidth may not be less 0" }
|
||||
if (columnWidth == 0) {
|
||||
return line
|
||||
}
|
||||
val length = line.length
|
||||
val delimiter = "<br/>".length
|
||||
var widthIndex = columnWidth
|
||||
val b = StringBuilder(line)
|
||||
var count = 0
|
||||
while (length > widthIndex) {
|
||||
var breakPoint = widthIndex + delimiter * count
|
||||
if (b[breakPoint - 1].isHighSurrogate() &&
|
||||
b[breakPoint].isLowSurrogate()
|
||||
) {
|
||||
// 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
|
||||
count++
|
||||
}
|
||||
return b.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.unifieddiff
|
||||
|
||||
import dev.gitlive.difflib.Predicate
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Tobias Warneke (t.warneke@gmx.net)
|
||||
*/
|
||||
class UnifiedDiff {
|
||||
var header: String? = null
|
||||
var tail: String? = null
|
||||
private set
|
||||
private val files: MutableList<UnifiedDiffFile> = ArrayList()
|
||||
fun addFile(file: UnifiedDiffFile) {
|
||||
files.add(file)
|
||||
}
|
||||
|
||||
fun getFiles(): List<UnifiedDiffFile> {
|
||||
return files
|
||||
}
|
||||
|
||||
fun setTailTxt(tailTxt: String?) {
|
||||
tail = tailTxt
|
||||
}
|
||||
|
||||
// @Throws(PatchFailedException::class)
|
||||
fun applyPatchTo(findFile: Predicate<String>, originalLines: List<String>): List<String> {
|
||||
val file = files.asSequence()
|
||||
.filter { diff: UnifiedDiffFile -> findFile(diff.fromFile!!) }
|
||||
.firstOrNull()
|
||||
return file?.patch?.applyTo(originalLines) ?: originalLines
|
||||
}
|
||||
|
||||
companion object {
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun from(header: String?, tail: String?, vararg files: UnifiedDiffFile): UnifiedDiff {
|
||||
val diff = UnifiedDiff()
|
||||
diff.header = header
|
||||
diff.setTailTxt(tail)
|
||||
for (file in files) {
|
||||
diff.addFile(file)
|
||||
}
|
||||
return diff
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.unifieddiff
|
||||
|
||||
import dev.gitlive.difflib.patch.Patch
|
||||
|
||||
/**
|
||||
* Data structure for one patched file from a unified diff file.
|
||||
*
|
||||
* @author Tobias Warneke (t.warneke@gmx.net)
|
||||
*/
|
||||
class UnifiedDiffFile {
|
||||
var diffCommand: String? = null
|
||||
var fromFile: String? = null
|
||||
var fromTimestamp: String? = null
|
||||
var toFile: String? = null
|
||||
var renameFrom: String? = null
|
||||
var renameTo: String? = null
|
||||
var toTimestamp: String? = null
|
||||
var index: String? = null
|
||||
var newFileMode: String? = null
|
||||
var deletedFileMode: String? = null
|
||||
var patch = Patch<String>()
|
||||
private set
|
||||
var additions: Int? = null
|
||||
var deletions: Int? = null
|
||||
var isNoNewLineAtTheEndOfTheFile = false
|
||||
var similarityIndex: Int? = null
|
||||
|
||||
companion object {
|
||||
@kotlin.jvm.JvmStatic
|
||||
fun from(fromFile: String?, toFile: String?, patch: Patch<String>): UnifiedDiffFile {
|
||||
val file = UnifiedDiffFile()
|
||||
file.fromFile = fromFile
|
||||
file.toFile = toFile
|
||||
file.patch = patch
|
||||
return file
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 dev.gitlive.difflib.unifieddiff
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Tobias Warneke (t.warneke@gmx.net)
|
||||
*/
|
||||
class UnifiedDiffParserException : RuntimeException {
|
||||
constructor() {}
|
||||
constructor(message: String?) : super(message) {}
|
||||
constructor(message: String?, cause: Throwable?) : super(message, cause) {}
|
||||
constructor(cause: Throwable?) : super(cause) {}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package dev.gitlive.difflib.unifieddiff
|
||||
|
||||
import dev.gitlive.difflib.*
|
||||
import dev.gitlive.difflib.patch.ChangeDelta
|
||||
import dev.gitlive.difflib.patch.Chunk
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Tobias Warneke (t.warneke@gmx.net)
|
||||
*/
|
||||
class UnifiedDiffReader internal constructor(lineReader: LineReader) {
|
||||
private val nextLine: LineReader
|
||||
private val data = UnifiedDiff()
|
||||
private val DIFF_COMMAND: UnifiedDiffLine = UnifiedDiffLine(true, "^diff\\s") { match: MatchResult, line: String ->
|
||||
processDiff(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val SIMILARITY_INDEX: UnifiedDiffLine = UnifiedDiffLine(true, "^similarity index (\\d+)%$") { match: MatchResult, line: String ->
|
||||
processSimilarityIndex(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val INDEX: UnifiedDiffLine = UnifiedDiffLine(true, "^index\\s[\\da-zA-Z]+\\.\\.[\\da-zA-Z]+(\\s(\\d+))?$") { match: MatchResult, line: String ->
|
||||
processIndex(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val FROM_FILE: UnifiedDiffLine = UnifiedDiffLine(true, "^---\\s") { match: MatchResult, line: String ->
|
||||
processFromFile(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val TO_FILE: UnifiedDiffLine = UnifiedDiffLine(true, "^\\+\\+\\+\\s") { match: MatchResult, line: String ->
|
||||
processToFile(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val RENAME_FROM: UnifiedDiffLine = UnifiedDiffLine(true, "^rename\\sfrom\\s(.+)$") { match: MatchResult, line: String ->
|
||||
processRenameFrom(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val RENAME_TO: UnifiedDiffLine = UnifiedDiffLine(true, "^rename\\sto\\s(.+)$") { match: MatchResult, line: String ->
|
||||
processRenameTo(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val NEW_FILE_MODE: UnifiedDiffLine = UnifiedDiffLine(true, "^new\\sfile\\smode\\s(\\d+)") { match: MatchResult, line: String ->
|
||||
processNewFileMode(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val DELETED_FILE_MODE: UnifiedDiffLine = UnifiedDiffLine(true, "^deleted\\sfile\\smode\\s(\\d+)") { match: MatchResult, line: String ->
|
||||
processDeletedFileMode(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val BINARY_FILE_CHANGED: UnifiedDiffLine = UnifiedDiffLine(true, "Binary files (.*) and (.*) differ") { match: MatchResult, line: String ->
|
||||
processBinaryFileChange(match, line)
|
||||
}
|
||||
private val CHUNK: UnifiedDiffLine = UnifiedDiffLine(false, UNIFIED_DIFF_CHUNK_REGEXP) { match: MatchResult, chunkStart: String ->
|
||||
processChunk(
|
||||
match,
|
||||
chunkStart
|
||||
)
|
||||
}
|
||||
private val LINE_NORMAL = UnifiedDiffLine("^\\s") { match: MatchResult, line: String ->
|
||||
processNormalLine(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val LINE_DEL = UnifiedDiffLine("^-") { match: MatchResult, line: String ->
|
||||
processDelLine(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private val LINE_ADD = UnifiedDiffLine("^\\+") { match: MatchResult, line: String ->
|
||||
processAddLine(
|
||||
match,
|
||||
line
|
||||
)
|
||||
}
|
||||
private var actualFile: UnifiedDiffFile? = null
|
||||
|
||||
// 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 suspend fun parse(): UnifiedDiff {
|
||||
var line: String? = nextLine()
|
||||
while (line != null) {
|
||||
var headerTxt = ""
|
||||
while (line != null) {
|
||||
headerTxt += if (validLine(
|
||||
line, DIFF_COMMAND, SIMILARITY_INDEX, INDEX,
|
||||
FROM_FILE, TO_FILE,
|
||||
RENAME_FROM, RENAME_TO,
|
||||
NEW_FILE_MODE, DELETED_FILE_MODE,
|
||||
CHUNK
|
||||
)
|
||||
) {
|
||||
break
|
||||
} else {
|
||||
"""
|
||||
$line
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
line = nextLine()
|
||||
}
|
||||
if ("" != headerTxt) {
|
||||
data.header = 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,
|
||||
NEW_FILE_MODE, DELETED_FILE_MODE,
|
||||
BINARY_FILE_CHANGED
|
||||
)
|
||||
) {
|
||||
throw UnifiedDiffParserException("expected file start line not found")
|
||||
}
|
||||
line = nextLine()
|
||||
}
|
||||
}
|
||||
if (line != null) {
|
||||
processLine(line, CHUNK)
|
||||
line = nextLine()
|
||||
var tempNewLine: String? = null
|
||||
while (line != null) {
|
||||
line = checkForNoNewLineAtTheEndOfTheFile(line)
|
||||
if (!processLine(line, LINE_NORMAL, LINE_ADD, LINE_DEL)) {
|
||||
// To help debugging tests
|
||||
line = nextLine()
|
||||
if (line != null && line.isNotEmpty()) {
|
||||
throw UnifiedDiffParserException("unlikely empty string found in CHUNK before: $line")
|
||||
}
|
||||
throw UnifiedDiffParserException("expected data line not found")
|
||||
}
|
||||
if (originalTxt.size == old_size && revisedTxt.size == new_size
|
||||
|| old_size == 0 && new_size == 0 && originalTxt.size == old_ln && revisedTxt.size == new_ln
|
||||
) {
|
||||
val isNormalLine = line?.let { NORMAL_LINE_TEXT.containsMatchIn(it) } ?: false
|
||||
if (!isNormalLine) {
|
||||
tempNewLine = nextLine()
|
||||
if (NO_NEW_LINE_TEXT != tempNewLine && actualFile!!.isNoNewLineAtTheEndOfTheFile) {
|
||||
revisedTxt.add("")
|
||||
new_size++
|
||||
} else if (NO_NEW_LINE_TEXT == tempNewLine && !actualFile!!.isNoNewLineAtTheEndOfTheFile) {
|
||||
originalTxt.add("")
|
||||
old_size++
|
||||
}
|
||||
}
|
||||
|
||||
finalizeChunk()
|
||||
break
|
||||
}
|
||||
line = nextLine()
|
||||
}
|
||||
line = tempNewLine ?: nextLine()
|
||||
line = checkForNoNewLineAtTheEndOfTheFile(line)
|
||||
}
|
||||
if (line == null || line.startsWith("--") && !line.startsWith("---")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
line = nextLine()
|
||||
if(line != null) {
|
||||
var tailTxt = ""
|
||||
while (line != null) {
|
||||
if (tailTxt.isNotEmpty()) {
|
||||
tailTxt += "\n"
|
||||
}
|
||||
tailTxt += line
|
||||
line = nextLine()
|
||||
}
|
||||
data.setTailTxt(tailTxt)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private suspend fun checkForNoNewLineAtTheEndOfTheFile(line: String?): String? {
|
||||
if (NO_NEW_LINE_TEXT == line) {
|
||||
actualFile!!.isNoNewLineAtTheEndOfTheFile = true
|
||||
return nextLine()
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
private suspend fun processLine(line: String?, vararg rules: UnifiedDiffLine): Boolean {
|
||||
if (line == null) {
|
||||
return false
|
||||
}
|
||||
for (rule in rules) {
|
||||
if (rule.processLine(line)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun validLine(line: String?, vararg rules: UnifiedDiffLine): Boolean {
|
||||
if (line == null) {
|
||||
return false
|
||||
}
|
||||
for (rule in rules) {
|
||||
if (rule.validLine(line)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun initFileIfNecessary() {
|
||||
check(!(originalTxt.isNotEmpty() || revisedTxt.isNotEmpty()))
|
||||
actualFile = null
|
||||
if (actualFile == null) {
|
||||
actualFile = UnifiedDiffFile()
|
||||
data.addFile(actualFile!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processDiff(match: MatchResult, line: String) {
|
||||
val fromTo = parseFileNames(line)
|
||||
actualFile!!.fromFile = fromTo[0]
|
||||
actualFile!!.toFile = fromTo[1]
|
||||
actualFile!!.diffCommand = line
|
||||
}
|
||||
|
||||
private fun processSimilarityIndex(match: MatchResult, line: String) {
|
||||
actualFile!!.similarityIndex = match.groupValues[1].toInt()
|
||||
}
|
||||
|
||||
private val originalTxt: MutableList<String> = mutableListOf()
|
||||
private val revisedTxt: MutableList<String> = mutableListOf()
|
||||
private val addLineIdxList: MutableList<Int> = mutableListOf()
|
||||
private val delLineIdxList: MutableList<Int> = mutableListOf()
|
||||
private var old_ln = 0
|
||||
private var old_size = 0
|
||||
private var new_ln = 0
|
||||
private var new_size = 0
|
||||
private var delLineIdx = 0
|
||||
private var addLineIdx = 0
|
||||
private var additions = 0
|
||||
private var deletions = 0
|
||||
|
||||
private fun finalizeChunk() {
|
||||
if (originalTxt.isNotEmpty() || revisedTxt.isNotEmpty()) {
|
||||
actualFile!!.patch.addDelta(
|
||||
ChangeDelta(
|
||||
Chunk(old_ln - 1, originalTxt, delLineIdxList),
|
||||
Chunk(new_ln - 1, revisedTxt, addLineIdxList)
|
||||
)
|
||||
)
|
||||
old_ln = 0
|
||||
new_ln = 0
|
||||
originalTxt.clear()
|
||||
revisedTxt.clear()
|
||||
addLineIdxList.clear()
|
||||
delLineIdxList.clear()
|
||||
actualFile!!.deletions = deletions
|
||||
deletions = 0
|
||||
delLineIdx = 0
|
||||
actualFile!!.additions = additions
|
||||
additions = 0
|
||||
addLineIdx = 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun processNormalLine(match: MatchResult, line: String) {
|
||||
val cline = line.substring(1)
|
||||
originalTxt.add(cline)
|
||||
revisedTxt.add(cline)
|
||||
delLineIdx++
|
||||
addLineIdx++
|
||||
}
|
||||
|
||||
private fun processAddLine(match: MatchResult, line: String) {
|
||||
val cline = line.substring(1)
|
||||
revisedTxt.add(cline)
|
||||
addLineIdx++
|
||||
additions++
|
||||
addLineIdxList.add(new_ln - 1 + addLineIdx)
|
||||
}
|
||||
|
||||
private fun processDelLine(match: MatchResult, line: String) {
|
||||
val cline = line.substring(1)
|
||||
originalTxt.add(cline)
|
||||
delLineIdx++
|
||||
deletions++
|
||||
delLineIdxList.add(old_ln - 1 + delLineIdx)
|
||||
}
|
||||
|
||||
private fun processChunk(match: MatchResult, chunkStart: String) {
|
||||
// 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 fun processIndex(match: MatchResult, line: String) {
|
||||
actualFile!!.index = line.substring(6)
|
||||
}
|
||||
|
||||
private fun processFromFile(match: MatchResult, line: String) {
|
||||
actualFile!!.fromFile = extractFileName(line)
|
||||
actualFile!!.fromTimestamp = extractTimestamp(line)
|
||||
}
|
||||
|
||||
private fun processToFile(match: MatchResult, line: String) {
|
||||
actualFile!!.toFile = extractFileName(line)
|
||||
actualFile!!.toTimestamp = extractTimestamp(line)
|
||||
}
|
||||
|
||||
private fun processRenameFrom(match: MatchResult, line: String) {
|
||||
actualFile!!.renameFrom = match.groupValues[1]
|
||||
}
|
||||
|
||||
private fun processRenameTo(match: MatchResult, line: String) {
|
||||
actualFile!!.renameTo = match.groupValues[1]
|
||||
}
|
||||
|
||||
private fun processNewFileMode(match: MatchResult, line: String) {
|
||||
actualFile!!.newFileMode = match.groupValues[1]
|
||||
}
|
||||
|
||||
private fun processDeletedFileMode(match: MatchResult, line: String) {
|
||||
actualFile!!.deletedFileMode = match.groupValues[1]
|
||||
}
|
||||
|
||||
private fun processBinaryFileChange(match: MatchResult, line: String) {
|
||||
// Nothing happens yet
|
||||
}
|
||||
|
||||
private fun extractFileName(_line: String): String {
|
||||
var line = _line
|
||||
if (TIMESTAMP_REGEXP.containsMatchIn(_line)) {
|
||||
line = line.substring(0, TIMESTAMP_REGEXP.find(_line)!!.range.first)
|
||||
}
|
||||
line = line.split("\t").toTypedArray()[0]
|
||||
return line.substring(4).replaceFirst("^(a|b|old|new)(\\/)?".toRegex(), "")
|
||||
.trim { it <= ' ' }
|
||||
}
|
||||
|
||||
private fun extractTimestamp(line: String): String? {
|
||||
return if (TIMESTAMP_REGEXP.containsMatchIn(line)) {
|
||||
TIMESTAMP_REGEXP.find(line)?.groupValues?.firstOrNull()
|
||||
} else null
|
||||
}
|
||||
|
||||
internal inner class UnifiedDiffLine {
|
||||
private val pattern: Regex
|
||||
private val command: BiConsumer<MatchResult, String>
|
||||
val isStopsHeaderParsing: Boolean
|
||||
|
||||
constructor(pattern: String, command: BiConsumer<MatchResult, String>) : this(false, pattern, command) {}
|
||||
constructor(stopsHeaderParsing: Boolean, pattern: String, command: BiConsumer<MatchResult, String>) {
|
||||
this.pattern = Regex(pattern)
|
||||
this.command = command
|
||||
isStopsHeaderParsing = stopsHeaderParsing
|
||||
}
|
||||
|
||||
constructor(stopsHeaderParsing: Boolean, pattern: Regex, command: BiConsumer<MatchResult, String>) {
|
||||
this.pattern = pattern
|
||||
this.command = command
|
||||
isStopsHeaderParsing = stopsHeaderParsing
|
||||
}
|
||||
|
||||
fun validLine(line: String): Boolean {
|
||||
return pattern.containsMatchIn(line)
|
||||
}
|
||||
|
||||
suspend fun processLine(line: String): Boolean {
|
||||
return if (pattern.containsMatchIn(line)) {
|
||||
command(pattern.find(line)!!, line)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "UnifiedDiffLine{pattern=$pattern, stopsHeaderParsing=$isStopsHeaderParsing}"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val UNIFIED_DIFF_CHUNK_REGEXP = """^@@\s+-(?:(\d+)(?:,(\d+))?)\s+\+(?:(\d+)(?:,(\d+))?)\s+@@""".toRegex()
|
||||
val TIMESTAMP_REGEXP = """(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.\d{3,})(?: [+-]\d+)?""".toRegex()
|
||||
val NORMAL_LINE_TEXT = """^\s""".toRegex()
|
||||
const val NO_NEW_LINE_TEXT = """\ No newline at end of file"""
|
||||
|
||||
fun parseFileNames(line: String?): Array<String> {
|
||||
val split = line!!.split(" ").toTypedArray()
|
||||
return arrayOf(
|
||||
split[2].replace("""^a/""".toRegex(), ""),
|
||||
split[3].replace("""^b/""".toRegex(), "")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
internal suspend fun parseUnifiedDiff(readLine: LineReader): UnifiedDiff {
|
||||
val parser = UnifiedDiffReader(readLine)
|
||||
return parser.parse()
|
||||
}
|
||||
|
||||
private fun toInteger(match: MatchResult, group: Int, defValue: Int): Int {
|
||||
return match.groups[group]?.value?.toIntOrNull() ?: defValue
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
nextLine = lineReader
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2019 java-diff-utils.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/**
|
||||
* This is the new implementation of UnifiedDiff Tools. This version is multi file aware.
|
||||
*
|
||||
*
|
||||
* To read a unified diff file you should use [UnifiedDiffReader.parseUnifiedDiff].
|
||||
* You will get a [UnifiedDiff] that holds all informations about the
|
||||
* diffs and the files.
|
||||
*
|
||||
*
|
||||
* To process the UnifiedDiff use [UnifiedDiffWriter.write].
|
||||
*/
|
||||
package dev.gitlive.difflib.unifieddiff
|
||||
|
||||
import dev.gitlive.difflib.unifieddiff.UnifiedDiff
|
||||
@@ -0,0 +1,209 @@
|
||||
package net.sergeych.merge3
|
||||
|
||||
import dev.gitlive.difflib.DiffUtils.diff
|
||||
import dev.gitlive.difflib.patch.AbstractDelta
|
||||
import dev.gitlive.difflib.patch.ChangeDelta
|
||||
import dev.gitlive.difflib.patch.DeleteDelta
|
||||
import dev.gitlive.difflib.patch.InsertDelta
|
||||
import net.sergeych.mp_logger.Log
|
||||
import net.sergeych.mp_logger.LogTag
|
||||
import net.sergeych.mp_logger.debug
|
||||
import net.sergeych.sprintf.sprintf
|
||||
|
||||
/**
|
||||
* Merge result. This version does not report (yet) conflicts, just merges it all.
|
||||
* @param merged the best merged resut. In the case of the conflict usually has both variants concatennated in place
|
||||
* @param changedAreas ranges where data were altered. could be used to highlight changes
|
||||
*/
|
||||
class MergeResult<T>(
|
||||
val merged: List<T>,
|
||||
val changedAreas: List<ClosedRange<Int>>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Perform 3-way merge. See [merge3] for details.
|
||||
*/
|
||||
private class Merge3<T>(val source: List<T>, val variantA: List<T>, val variantB: List<T>) :
|
||||
LogTag("MRG3", Log.Level.INFO) {
|
||||
|
||||
private val reindex = source.indices.toMutableList()
|
||||
private val changeCount = MutableList(source.size) { 0 }
|
||||
|
||||
val result = source.toMutableList()
|
||||
|
||||
private fun trace() {
|
||||
|
||||
debug { result.indices.joinToString("") { "%3d".sprintf(it) } }
|
||||
debug { result.joinToString("") { "%3s".sprintf(it.toString()) } }
|
||||
debug {
|
||||
changeCount.joinToString("") {
|
||||
when (it) {
|
||||
1 -> " *"
|
||||
0 -> " ."
|
||||
2 -> " !"
|
||||
else -> " !$it"
|
||||
}
|
||||
}
|
||||
}
|
||||
debug { reindex.joinToString("") { if (it < 0) " x" else "%3d".sprintf(it) } }
|
||||
|
||||
}
|
||||
|
||||
private fun findPosition(sourcePosition: Int): Int {
|
||||
var position = if (sourcePosition < reindex.size) reindex[sourcePosition] else reindex.last() + 1
|
||||
// found position could be already deleted
|
||||
if (position < 0) {
|
||||
// we look for leftmost position in the deleted block:
|
||||
debug { "@$sourcePosition is DELETED, look for the next" }
|
||||
var i = sourcePosition
|
||||
while (++i < reindex.size) {
|
||||
position = reindex[i]
|
||||
if (position >= 0) break
|
||||
}
|
||||
// if we hit the end, lets append to the old end, but we still don't want to loose
|
||||
// changes:
|
||||
if (position < 0) position = reindex.lastOrNull { it >= 0 }?.let { it + 1 } ?: 0
|
||||
}
|
||||
debug { "found position: $position" }
|
||||
return position
|
||||
}
|
||||
|
||||
private fun insertAt(sourcePosition: Int, fragment: List<T>) {
|
||||
debug { "inserting $fragment @$sourcePosition" }
|
||||
// position could be inside or after the end of the source string:
|
||||
val position = findPosition(sourcePosition)
|
||||
result.addAll(position, fragment)
|
||||
for (k in sourcePosition until reindex.size)
|
||||
if (reindex[k] >= 0)
|
||||
reindex[k] += fragment.size
|
||||
for (k in position until position + fragment.size)
|
||||
changeCount.add(k, 1)
|
||||
|
||||
}
|
||||
|
||||
private fun deleteAt(sourcePosition: Int, count: Int) {
|
||||
debug { "deleting $count elements @$sourcePosition" }
|
||||
val position = findPosition(sourcePosition)
|
||||
var sp = sourcePosition
|
||||
for (i in position until position + count) {
|
||||
// it this position is already removed, do nothing
|
||||
if (reindex[sp] < 0) continue
|
||||
reindex[sp++] = -1
|
||||
result.removeAt(position)
|
||||
changeCount.removeAt(position)
|
||||
}
|
||||
while (sp < reindex.size) {
|
||||
reindex[sp].let { if (it > 0) reindex[sp] = it - count }
|
||||
sp++
|
||||
}
|
||||
}
|
||||
|
||||
private val conflicts = mutableListOf<ClosedRange<Int>>()
|
||||
private val updates = mutableListOf<ClosedRange<Int>>()
|
||||
|
||||
private fun applyDelta(delta: AbstractDelta<T>) {
|
||||
when (delta) {
|
||||
is InsertDelta<T> -> {
|
||||
insertAt(delta.source.position, delta.target.lines)
|
||||
}
|
||||
|
||||
is ChangeDelta<T> -> {
|
||||
deleteAt(delta.source.position, delta.source.size())
|
||||
trace()
|
||||
insertAt(delta.source.position, delta.target.lines)
|
||||
}
|
||||
|
||||
is DeleteDelta<T> -> {
|
||||
deleteAt(delta.source.position, delta.source.size())
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw Exception("Can't apply: $delta")
|
||||
}
|
||||
}
|
||||
debug { "Applied: $delta, result:" }
|
||||
trace()
|
||||
}
|
||||
|
||||
|
||||
fun perform(): MergeResult<T> {
|
||||
val dA = diff(source, variantA).getDeltas()
|
||||
val dB = diff(source, variantB).getDeltas()
|
||||
|
||||
debug { "dA: $dA" }
|
||||
debug { "dB: $dB" }
|
||||
|
||||
|
||||
trace()
|
||||
|
||||
debug { "adding first set" }
|
||||
for (d in dA) {
|
||||
debug { "adding dA $d" }
|
||||
applyDelta(d)
|
||||
}
|
||||
for (d in dB) {
|
||||
debug { "adding dB $d" }
|
||||
applyDelta(d)
|
||||
}
|
||||
|
||||
// detect ranges
|
||||
var conflictStart = -1
|
||||
var changeStart = -1
|
||||
fun closeConflict(index: Int) {
|
||||
if (conflictStart >= 0) {
|
||||
conflicts.add(conflictStart until index)
|
||||
conflictStart = -1
|
||||
}
|
||||
}
|
||||
|
||||
fun closeUpdate(index: Int) {
|
||||
if (changeStart >= 0) {
|
||||
updates.add(changeStart until index)
|
||||
changeStart = -1
|
||||
}
|
||||
}
|
||||
|
||||
for ((index, count) in changeCount.withIndex()) {
|
||||
when (count) {
|
||||
0 -> {
|
||||
// close conflict or update if was open
|
||||
closeConflict(index)
|
||||
closeUpdate(index)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
// could be end of the conflict
|
||||
closeConflict(index)
|
||||
// open change if not opened
|
||||
if (changeStart < 0) changeStart = index
|
||||
}
|
||||
|
||||
2 -> {
|
||||
// could be an end of the change
|
||||
closeUpdate(index)
|
||||
// could be opening of the conflict
|
||||
if (conflictStart < 0) conflictStart = index
|
||||
}
|
||||
|
||||
else -> {
|
||||
trace()
|
||||
throw RuntimeException("internal error: invalud change counter @$index:$count")
|
||||
}
|
||||
}
|
||||
}
|
||||
closeConflict(changeCount.size)
|
||||
closeUpdate(changeCount.size)
|
||||
|
||||
return MergeResult<T>(result, updates)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lossless (optimistically) 3-way merge. __this version does not report conflicts!__
|
||||
* Perform lossless smart merge of the [source] updated to [a] and to [b] respectively.
|
||||
* See [MergeResult] for interpreting results
|
||||
*/
|
||||
fun <T> merge3(source: List<T>, a: List<T>, b: List<T>, showDebug: Boolean = false): MergeResult<T> =
|
||||
Merge3(source, a, b).also {
|
||||
if( showDebug ) it.logLevel = Log.Level.DEBUG
|
||||
}.perform()
|
||||
@@ -0,0 +1,49 @@
|
||||
import net.sergeych.merge3.merge3
|
||||
import net.sergeych.mp_logger.Log
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
val List<Char>.str: String get() = joinToString("")
|
||||
|
||||
class BasicTest {
|
||||
@Test
|
||||
fun testMergeNoConflicts() {
|
||||
val src = "Hello world".toList()
|
||||
val a = "Hello cruel world!".toList()
|
||||
val b = "Bye world!!".toList()
|
||||
|
||||
val m = merge3(src, b, a)
|
||||
println(m.merged.str)
|
||||
// println(m.conflicts)
|
||||
println(m.changedAreas)
|
||||
assertEquals("Bye cruel world!!!", m.merged.str)
|
||||
// assertTrue(m.noConflicts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMergeNoConflicts2() {
|
||||
val src = "Hello world".toList()
|
||||
val a = "Hello friend".toList()
|
||||
val b = "Bye world".toList()
|
||||
|
||||
val m = merge3(src, b, a)
|
||||
println(m.merged.str)
|
||||
// println(m.conflicts)
|
||||
println(m.changedAreas)
|
||||
assertEquals("Bye friend", m.merged.str)
|
||||
// assertTrue(m.noConflicts)
|
||||
}
|
||||
@Test
|
||||
fun testMergeWithConflicts() {
|
||||
Log.connectConsole()
|
||||
val src = "Hello world".toList()
|
||||
val a = "Hello 123".toList()
|
||||
val b = "Hello 456".toList()
|
||||
|
||||
val m = merge3(src, b, a, false)
|
||||
println(m.merged.str)
|
||||
// println(m.conflicts)
|
||||
println(m.changedAreas)
|
||||
assertEquals("Hello 123456", m.merged.str)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user