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()