262 lines
8.3 KiB
Kotlin
262 lines
8.3 KiB
Kotlin
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. See also [blocks] for
|
|
* another representation of the merged data.
|
|
*
|
|
* @param merged the best merged data. 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<IntRange>,
|
|
) {
|
|
val blocks: List<MergedBlock<T>> by lazy {
|
|
if (changedAreas.isEmpty())
|
|
listOf(MergedBlock.Unchanged(merged))
|
|
else {
|
|
val result = mutableListOf<MergedBlock<T>>()
|
|
var start = 0
|
|
for (r in changedAreas) {
|
|
if (start != r.start) {
|
|
result.add(MergedBlock.Unchanged(merged.slice(start until r.start)))
|
|
}
|
|
if (!r.isEmpty()) {
|
|
result.add(MergedBlock.Resolved(merged.slice(r)))
|
|
}
|
|
start = r.endInclusive + 1
|
|
}
|
|
if (start < merged.size) {
|
|
result.add(MergedBlock.Unchanged(merged.slice(start until merged.size)))
|
|
}
|
|
result
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge3 sequential result block.
|
|
*/
|
|
sealed class MergedBlock<T> {
|
|
/**
|
|
* Merged data. Int the case of the conflict, the concatenated conflicting data
|
|
*/
|
|
abstract val data: List<T>
|
|
|
|
/**
|
|
* Data that are equals in both variants and therefore was not altered
|
|
*/
|
|
data class Unchanged<T>(override val data: List<T>) : MergedBlock<T>()
|
|
|
|
/**
|
|
* The portion of data that was merged without conflicts
|
|
*/
|
|
data class Resolved<T>(override val data: List<T>) : MergedBlock<T>()
|
|
|
|
/**
|
|
* The portion of data that can't be merged due to the conflicting changes.
|
|
* __Note it is not yet used.__
|
|
*/
|
|
@Suppress("unused")
|
|
data class Conflict<T>(val variantA: List<T>, val variantB: List<T>) : MergedBlock<T>() {
|
|
override val data = variantA + variantB
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 updates = mutableListOf<IntRange>()
|
|
|
|
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() |