fix #1 fix #2 fix #3
fixed and enhanced merge
This commit is contained in:
Sergey Chernov 2024-03-20 00:08:20 +01:00
parent 6a67a4e840
commit 446a7adf6f
11 changed files with 1425 additions and 400 deletions

View File

@ -1,10 +1,10 @@
plugins {
kotlin("multiplatform") version "1.7.21"
kotlin("multiplatform") version "1.9.22"
`maven-publish`
}
group = "net.sergeych"
version = "0.0.8-SNAPSHOT"
version = "0.0.10"
repositories {
mavenCentral()
@ -14,7 +14,7 @@ repositories {
kotlin {
jvm {
jvmToolchain(8)
jvmToolchain(11)
withJava()
testRuns["test"].executionTask.configure {
useJUnitPlatform()
@ -29,25 +29,63 @@ kotlin {
}
}
}
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
// val hostOs = System.getProperty("os.name")
// val isMingwX64 = hostOs.startsWith("Windows")
// val nativeTarget = when {
// hostOs == "Mac OS X" -> macosX64("native")
// hostOs == "Linux" -> linuxX64("native")
// isMingwX64 -> mingwX64("native")
// else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
// }
wasmJs {
browser()
binaries.executable()
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "morozilko_lib"
isStatic = true
}
}
listOf(
macosX64(),
macosArm64()
).forEach {
it.binaries.framework {
baseName = "morozilko_lib"
isStatic = true
}
}
linuxX64 {
binaries.staticLib {
baseName = "morozilko_lib"
}
}
mingwX64 {
binaries.staticLib {
baseName = "morozilko_lib"
}
}
sourceSets {
val commonMain by getting {
dependencies {
// implementation("dev.gitlive:kotlin-diff-utils:4.1.4")
// implementation("net.sergeych:mp_stools:[1.4.7,)")
}
}
val commonTest by getting {
dependencies {
implementation("net.sergeych:mp_stools:[1.3.4,)")
implementation(kotlin("test"))
}
}
@ -55,8 +93,8 @@ kotlin {
val jvmTest by getting
val jsMain by getting
val jsTest by getting
val nativeMain by getting
val nativeTest by getting
// val nativeMain by getting
// val nativeTest by getting
}
publishing {

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ class MeyersDiff<T> : DiffAlgorithmI<T> {
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)
val result = buildRevision(path)
progress?.diffEnd()
return result
}
@ -116,7 +116,7 @@ class MeyersDiff<T> : DiffAlgorithmI<T> {
* @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> {
private fun buildRevision(actualPath: PathNode): List<Change> {
var path: PathNode? = actualPath
val changes: MutableList<Change> = ArrayList()
if (path!!.isSnake) {

View File

@ -62,7 +62,7 @@ abstract class AbstractDelta<T>(val type: DeltaType, val source: Chunk<T>, val t
if (this::class != other::class) {
return false
}
val other = other as AbstractDelta<*>
other as AbstractDelta<*>
if (source != other.source) {
return false
}
@ -70,4 +70,14 @@ abstract class AbstractDelta<T>(val type: DeltaType, val source: Chunk<T>, val t
false
} else type == other.type
}
/**
* If this delta is partially performed by [existing], update it si it only changes necessary
* data. If this delta copies completely an existing delta, return null.
*
* Base class implementation only removes complete dupes
*/
open fun optimizeAgainst(existing: AbstractDelta<T>): AbstractDelta<T>? {
return if( this == existing ) null else this
}
}

View File

@ -15,6 +15,8 @@
*/
package dev.gitlive.difflib.patch
import net.sergeych.merge3.overlaps
/**
* Holds the information about the part of text involved in the diff process
*
@ -28,7 +30,7 @@ package dev.gitlive.difflib.patch
*
*
* @author [](dm.naumenko@gmail.com>Dmitry Naumenko</a>
@param <T> The type of the compared elements in the 'lines'.
* @param <T> The type of the compared elements in the 'lines'.
) */
class Chunk<T> {
@ -130,8 +132,8 @@ class Chunk<T> {
if (this::class != other::class) {
return false
}
val other = other as Chunk<*>?
if (lines != other!!.lines) {
other as Chunk<*>
if (lines != other.lines) {
return false
}
return position == other.position
@ -140,4 +142,11 @@ class Chunk<T> {
override fun toString(): String {
return "[position: " + position + ", size: " + size() + ", lines: " + lines + "]"
}
/**
* Range that this chunk covers (start incusive, end non inclusive)
*/
fun range() = position..< position+size()
fun overlaps(target: Chunk<T>): Boolean = range() overlaps target.range()
}

View File

@ -54,4 +54,35 @@ class InsertDelta<T>
override fun withChunks(original: Chunk<T>, revised: Chunk<T>): AbstractDelta<T> {
return InsertDelta(original, revised)
}
/**
* Eliminates inserting the same text in the same positions
*/
override fun optimizeAgainst(existing: AbstractDelta<T>): AbstractDelta<T>? {
return super.optimizeAgainst(existing)?.let { x ->
if (existing !is InsertDelta)
x
else {
// this algorithm only eliminates deltas that have the common prefix:
if (existing.target.position != target.position)
return this
// remove common prefix:
var offset = 0
while (existing.target.lines[offset] == target.lines[offset]) {
offset++
if (offset >= target.lines.size || offset >= existing.target.lines.size) break
}
if (offset >= target.lines.size) {
// removed completely
return null
}
// removed a part:
val tail = target.lines.drop(offset)
return InsertDelta(
Chunk(source.position, source.lines.drop(offset), source.changePosition?.let { it + offset }),
Chunk(target.position + offset, tail, target.changePosition?.let { it + offset })
)
}
}
}
}

View File

@ -56,7 +56,7 @@ class DiffRow(
if (this::class != other::class) {
return false
}
val other = other as DiffRow
other as DiffRow
if (newLine != other.newLine) {
return false
}

View File

@ -12,84 +12,71 @@ import dev.gitlive.difflib.patch.Chunk
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 ->
private val DIFF_COMMAND: UnifiedDiffLine = UnifiedDiffLine(true, "^diff\\s") { _, line: String ->
processDiff(
match,
line
)
}
private val SIMILARITY_INDEX: UnifiedDiffLine = UnifiedDiffLine(true, "^similarity index (\\d+)%$") { match: MatchResult, line: String ->
private val SIMILARITY_INDEX: UnifiedDiffLine = UnifiedDiffLine(true, "^similarity index (\\d+)%$") { match: MatchResult, _ ->
processSimilarityIndex(
match,
line
match
)
}
private val INDEX: UnifiedDiffLine = UnifiedDiffLine(true, "^index\\s[\\da-zA-Z]+\\.\\.[\\da-zA-Z]+(\\s(\\d+))?$") { match: MatchResult, line: String ->
private val INDEX: UnifiedDiffLine = UnifiedDiffLine(true, "^index\\s[\\da-zA-Z]+\\.\\.[\\da-zA-Z]+(\\s(\\d+))?$") { _: MatchResult, line: String ->
processIndex(
match,
line
)
}
private val FROM_FILE: UnifiedDiffLine = UnifiedDiffLine(true, "^---\\s") { match: MatchResult, line: String ->
private val FROM_FILE: UnifiedDiffLine = UnifiedDiffLine(true, "^---\\s") { _, line: String ->
processFromFile(
match,
line
)
}
private val TO_FILE: UnifiedDiffLine = UnifiedDiffLine(true, "^\\+\\+\\+\\s") { match: MatchResult, line: String ->
private val TO_FILE: UnifiedDiffLine = UnifiedDiffLine(true, "^\\+\\+\\+\\s") { _, line: String ->
processToFile(
match,
line
)
}
private val RENAME_FROM: UnifiedDiffLine = UnifiedDiffLine(true, "^rename\\sfrom\\s(.+)$") { match: MatchResult, line: String ->
private val RENAME_FROM: UnifiedDiffLine = UnifiedDiffLine(true, "^rename\\sfrom\\s(.+)$") { match: MatchResult, _: String ->
processRenameFrom(
match,
line
match
)
}
private val RENAME_TO: UnifiedDiffLine = UnifiedDiffLine(true, "^rename\\sto\\s(.+)$") { match: MatchResult, line: String ->
private val RENAME_TO: UnifiedDiffLine = UnifiedDiffLine(true, "^rename\\sto\\s(.+)$") { match: MatchResult, _: String ->
processRenameTo(
match,
line
match
)
}
private val NEW_FILE_MODE: UnifiedDiffLine = UnifiedDiffLine(true, "^new\\sfile\\smode\\s(\\d+)") { match: MatchResult, line: String ->
private val NEW_FILE_MODE: UnifiedDiffLine = UnifiedDiffLine(true, "^new\\sfile\\smode\\s(\\d+)") { match: MatchResult, _: String ->
processNewFileMode(
match,
line
match
)
}
private val DELETED_FILE_MODE: UnifiedDiffLine = UnifiedDiffLine(true, "^deleted\\sfile\\smode\\s(\\d+)") { match: MatchResult, line: String ->
private val DELETED_FILE_MODE: UnifiedDiffLine = UnifiedDiffLine(true, "^deleted\\sfile\\smode\\s(\\d+)") { match: MatchResult, _: String ->
processDeletedFileMode(
match,
line
match
)
}
private val BINARY_FILE_CHANGED: UnifiedDiffLine = UnifiedDiffLine(true, "Binary files (.*) and (.*) differ") { match: MatchResult, line: String ->
processBinaryFileChange(match, line)
private val BINARY_FILE_CHANGED: UnifiedDiffLine = UnifiedDiffLine(true, "Binary files (.*) and (.*) differ") { _, _ ->
processBinaryFileChange()
}
private val CHUNK: UnifiedDiffLine = UnifiedDiffLine(false, UNIFIED_DIFF_CHUNK_REGEXP) { match: MatchResult, chunkStart: String ->
private val CHUNK: UnifiedDiffLine = UnifiedDiffLine(false, UNIFIED_DIFF_CHUNK_REGEXP) { match: MatchResult, _: String ->
processChunk(
match,
chunkStart
match
)
}
private val LINE_NORMAL = UnifiedDiffLine("^\\s") { match: MatchResult, line: String ->
private val LINE_NORMAL = UnifiedDiffLine("^\\s") { _: MatchResult, line: String ->
processNormalLine(
match,
line
)
}
private val LINE_DEL = UnifiedDiffLine("^-") { match: MatchResult, line: String ->
private val LINE_DEL = UnifiedDiffLine("^-") { _: MatchResult, line: String ->
processDelLine(
match,
line
)
}
private val LINE_ADD = UnifiedDiffLine("^\\+") { match: MatchResult, line: String ->
private val LINE_ADD = UnifiedDiffLine("^\\+") { _: MatchResult, line: String ->
processAddLine(
match,
line
)
}
@ -238,14 +225,14 @@ class UnifiedDiffReader internal constructor(lineReader: LineReader) {
}
}
private fun processDiff(match: MatchResult, line: String) {
private fun processDiff(line: String) {
val fromTo = parseFileNames(line)
actualFile!!.fromFile = fromTo[0]
actualFile!!.toFile = fromTo[1]
actualFile!!.diffCommand = line
}
private fun processSimilarityIndex(match: MatchResult, line: String) {
private fun processSimilarityIndex(match: MatchResult) {
actualFile!!.similarityIndex = match.groupValues[1].toInt()
}
@ -285,7 +272,7 @@ class UnifiedDiffReader internal constructor(lineReader: LineReader) {
}
}
private fun processNormalLine(match: MatchResult, line: String) {
private fun processNormalLine(line: String) {
val cline = line.substring(1)
originalTxt.add(cline)
revisedTxt.add(cline)
@ -293,7 +280,7 @@ class UnifiedDiffReader internal constructor(lineReader: LineReader) {
addLineIdx++
}
private fun processAddLine(match: MatchResult, line: String) {
private fun processAddLine(line: String) {
val cline = line.substring(1)
revisedTxt.add(cline)
addLineIdx++
@ -301,7 +288,7 @@ class UnifiedDiffReader internal constructor(lineReader: LineReader) {
addLineIdxList.add(new_ln - 1 + addLineIdx)
}
private fun processDelLine(match: MatchResult, line: String) {
private fun processDelLine(line: String) {
val cline = line.substring(1)
originalTxt.add(cline)
delLineIdx++
@ -309,7 +296,7 @@ class UnifiedDiffReader internal constructor(lineReader: LineReader) {
delLineIdxList.add(old_ln - 1 + delLineIdx)
}
private fun processChunk(match: MatchResult, chunkStart: String) {
private fun processChunk(match: MatchResult) {
// finalizeChunk();
old_ln = toInteger(match, 1, 1)
old_size = toInteger(match, 2, 1)
@ -323,37 +310,37 @@ class UnifiedDiffReader internal constructor(lineReader: LineReader) {
}
}
private fun processIndex(match: MatchResult, line: String) {
private fun processIndex(line: String) {
actualFile!!.index = line.substring(6)
}
private fun processFromFile(match: MatchResult, line: String) {
private fun processFromFile(line: String) {
actualFile!!.fromFile = extractFileName(line)
actualFile!!.fromTimestamp = extractTimestamp(line)
}
private fun processToFile(match: MatchResult, line: String) {
private fun processToFile(line: String) {
actualFile!!.toFile = extractFileName(line)
actualFile!!.toTimestamp = extractTimestamp(line)
}
private fun processRenameFrom(match: MatchResult, line: String) {
private fun processRenameFrom(match: MatchResult) {
actualFile!!.renameFrom = match.groupValues[1]
}
private fun processRenameTo(match: MatchResult, line: String) {
private fun processRenameTo(match: MatchResult) {
actualFile!!.renameTo = match.groupValues[1]
}
private fun processNewFileMode(match: MatchResult, line: String) {
private fun processNewFileMode(match: MatchResult) {
actualFile!!.newFileMode = match.groupValues[1]
}
private fun processDeletedFileMode(match: MatchResult, line: String) {
private fun processDeletedFileMode(match: MatchResult) {
actualFile!!.deletedFileMode = match.groupValues[1]
}
private fun processBinaryFileChange(match: MatchResult, line: String) {
private fun processBinaryFileChange() {
// Nothing happens yet
}

View File

@ -105,7 +105,6 @@ private class Merge3<T>(
}
// private fun trace() {
//
// debug { result.indices.joinToString("") { "%3d".sprintf(it) } }
// debug { result.joinToString("") { "%3s".sprintf(it.toString()) } }
// debug {
@ -205,22 +204,25 @@ private class Merge3<T>(
fun perform(): MergeResult<T> {
val dA = diff(source, variantA).getDeltas()
val dB = diff(source, variantB).getDeltas()
val dB = removeDupes(diff(source, variantB).getDeltas(), dA)
debug { "dA: $dA" }
debug { "dB: $dB" }
// trace()
debug { "adding first set" }
for (d in dA) {
debug { "adding dA $d" }
applyDelta(d)
}
// optimized nust be applied first
for (d in dB) {
debug { "adding dB $d" }
applyDelta(d)
}
// then apply full
for (d in dA) {
debug { "adding dA $d" }
applyDelta(d)
}
// detect ranges
var conflictStart = -1
@ -272,6 +274,25 @@ private class Merge3<T>(
return MergeResult<T>(result, updates, reindex)
}
/**
* Remove deltas from [deltas] that are already included into [existing]. It might require
* modifying existing deltas or removing it completely
*/
private fun removeDupes(
deltas: List<AbstractDelta<T>>,
existing: List<AbstractDelta<T>>,
): List<AbstractDelta<T>> {
// partial solutions: complete removal
return deltas.filter { it !in existing }.mapNotNull { s0 ->
var s: AbstractDelta<T>? = s0
for (x in existing) {
s = s?.optimizeAgainst(x)
if( s == null ) break
}
s
}
}
}
/**

View File

@ -0,0 +1,18 @@
package net.sergeych.merge3
/**
* Check that other is inside this (inclusive)
*/
operator fun <T: Comparable<T>>ClosedRange<T>.contains(other: ClosedRange<T>): Boolean =
other.start >= start && other.endInclusive <= endInclusive
/**
* Check that [other] overlaps (e.g., other has non-empty intersection with this), this also
* includes the case when other is inside this or this is inside other completely. Use [contains] to exclude
* the containing.
*/
infix fun <T: Comparable<T>>ClosedRange<T>.overlaps(other: ClosedRange<T>): Boolean {
return start in other || endInclusive in other
}

View File

@ -1,23 +1,23 @@
import net.sergeych.merge3.MergeResult
import net.sergeych.merge3.MergedBlock
import net.sergeych.merge3.merge3
import net.sergeych.mp_logger.Log
import net.sergeych.sprintf.sprintf
//import net.sergeych.mp_logger.Log
//import net.sergeych.sprintf.sprintf
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
val List<Char>.str: String get() = joinToString("")
fun <T>MergeResult<T>.toTrace(source: List<T>): String {
val r = StringBuilder()
r.append(merged.indices.joinToString("") { "%3d".sprintf(it) } + "\n")
r.append(merged.joinToString("") { "%3s".sprintf(it) } + "\n")
r.append("---- source ----\n")
r.append(source.joinToString("") { "%3s".sprintf(it) } + "\n")
r.append(sourceIndices.joinToString("") { "%3s".sprintf(it) } + "\n")
return r.toString()
}
// fun <T>MergeResult<T>.toTrace(source: List<T>): String {
// val r = StringBuilder()
// r.append(merged.indices.joinToString("") { "%3d".sprintf(it) } + "\n")
// r.append(merged.joinToString("") { "%3s".sprintf(it) } + "\n")
// r.append("---- source ----\n")
// r.append(source.joinToString("") { "%3s".sprintf(it) } + "\n")
// r.append(sourceIndices.joinToString("") { "%3s".sprintf(it) } + "\n")
// return r.toString()
// }
class BasicTest {
@Test
@ -27,25 +27,25 @@ class BasicTest {
val b = "Bye world!!".toList()
val m = merge3(src, b, a)
println(m.merged.str)
// println(m.merged.str)
// println(m.conflicts)
println(m.changedAreas)
// println(m.changedAreas)
assertEquals("Bye cruel world!!!", m.merged.str)
// assertTrue(m.noConflicts)
}
@Test
fun testMergeNoConflicts3() {
Log.connectConsole()
// Log.connectConsole()
val src = "Hello, world!".toList()
val a = "Hello, friend!".toList()
val b = "Bye world!".toList()
val m = merge3(src, a, b, true)
println(m.merged.str)
val m = merge3(src, a, b)
// println(m.merged.str)
// println(m.conflicts)
println(m.changedAreas)
// println(m.changedAreas)
assertEquals("Bye friend!", m.merged.str)
println(m.toTrace(src))
// println(m.toTrace(src))
// assertTrue(m.noConflicts)
}
@ -56,103 +56,142 @@ class BasicTest {
val b = "Bye world".toList()
val m = merge3(src, b, a)
println(m.merged.str)
// println(m.merged.str)
// println(m.conflicts)
println(m.changedAreas)
// println(m.changedAreas)
assertEquals("Bye friend", m.merged.str)
// assertTrue(m.noConflicts)
println(m.blocks)
// println(m.blocks)
}
@Test
fun testMergeWithConflicts() {
Log.connectConsole()
// 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)
val m = merge3(src, b, a)
// println(m.merged.str)
// println(m.conflicts)
println(m.changedAreas)
assertEquals("Hello 123456", m.merged.str)
// println(m.changedAreas)
assertEquals("Hello 456123", m.merged.str)
}
@Test
fun testMergeIndexes() {
Log.connectConsole()
// Log.connectConsole()
val src = "Hello".toList()
val a = "123 Hello".toList()
val b = "456 Hello".toList()
val m = merge3(src, b, a, true)
val m = merge3(src, b, a)
// println(m.toTrace(src))
// println(m.changedAreas)
// for( b in m.blocks )
// println(b)
val unchanged = m.blocks.last() as MergedBlock.Unchanged
println(unchanged)
// println(unchanged)
assertEquals(0, unchanged.referenceIndex)
}
@Test
fun testMergeIndexes2() {
Log.connectConsole()
// Log.connectConsole()
val src = "Hello".toList()
val a = "1Hello".toList()
val b = "2Hello".toList()
val m = merge3(src, b, a, true)
val m = merge3(src, b, a)
val unchanged = m.blocks.last() as MergedBlock.Unchanged
println(unchanged)
// println(unchanged)
assertEquals(0, unchanged.referenceIndex)
}
@Test
fun testMergeFilter() {
Log.connectConsole()
// Log.connectConsole()
val source = "lxcvv".toList()
val our = "lssdfasdwerwev".toList()
val their = "lxcvasdfasdfs".toList()
val result = merge3(source, their, our)
println("got result: ${result.toTrace(source)}")
println("got result: ${result.blocks}")
// println("got result: ${result.toTrace(source)}")
// println("got result: ${result.blocks}")
result.blocks.filter { it is MergedBlock.Unchanged }
assertTrue(true)
}
@Test
fun testMergeEmpty() {
Log.connectConsole()
// Log.connectConsole()
val source = "".toList()
val our = "".toList()
val their = "".toList()
val result = merge3(source, their, our)
println("got result: ${result.toTrace(source)}")
println("got result: ${result.blocks}")
// println("got result: ${result.toTrace(source)}")
// println("got result: ${result.blocks}")
result.blocks.filter { it is MergedBlock.Unchanged }
assertTrue(true)
}
@Test
fun testMergeUnchanged() {
Log.connectConsole()
fun testMergeUnchanged1() {
val source = "abc".toList()
val their = "abDEF".toList()
val our = "abcGHY".toList()
val their = "abcDEF".toList()
val our = "abcDEF".toList()
val result = merge3(source, their, our)
println(result.merged.str)
assertTrue(false)
// println(result.merged.str)
// println(result.blocks)
assertEquals("abcDEF", result.merged.str)
}
@Test
fun testMergeConflictOrder() {
fun testMergePartial1() {
val source = "abc".toList()
val their = "abcDEF1".toList()
val our = "abcDEF".toList()
val result = merge3(source, their, our)
// println(result.merged.str)
// println(result.blocks)
assertEquals("abcDEF1", result.merged.str)
}
@Test
fun testMergePartial2() {
val source = "abc".toList()
val their = "abcDEF".toList()
val our = "abcDEF1".toList()
val result = merge3(source, their, our)
// println(result.merged.str)
// println(result.blocks)
assertEquals("abcDEF1", result.merged.str)
}
@Test
fun testMergeConflictOrder1() {
// Log.connectConsole()
val source = "abc".toList()
val their = "abcDEF".toList()
val our = "abcGHY".toList()
val result = merge3(source, their, our)
// println(result.merged.str)
// println(result.blocks)
// the order is unexpected:
assertTrue { result.merged.str in listOf("abcDEFGHY", "abcGHYDEF") }
}
@Test
fun testMergeConflictOrder2() {
val source = "ABAB".toList()
val our = "ACAC".toList()
val their = "ADAD".toList()
val result = merge3(source, their, our)
println(result.merged.str)
assertTrue(false)
// println(result.merged.str)
// The order is undetermied:
assertTrue(result.merged.str in listOf("ACDADC", "ADCACD", "ADCADC", "ACDACD"))
}
}