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( val merged: List, val changedAreas: List, ) { val blocks: List> by lazy { if (changedAreas.isEmpty()) listOf(MergedBlock.Unchanged(merged)) else { val result = mutableListOf>() 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 { /** * Merged data. Int the case of the conflict, the concatenated conflicting data */ abstract val data: List /** * Data that are equals in both variants and therefore was not altered */ data class Unchanged(override val data: List) : MergedBlock() /** * The portion of data that was merged without conflicts */ data class Resolved(override val data: List) : MergedBlock() /** * 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(val variantA: List, val variantB: List) : MergedBlock() { override val data = variantA + variantB } } /** * Perform 3-way merge. See [merge3] for details. */ private class Merge3(val source: List, val variantA: List, val variantB: List) : 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) { 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() private fun applyDelta(delta: AbstractDelta) { when (delta) { is InsertDelta -> { insertAt(delta.source.position, delta.target.lines) } is ChangeDelta -> { deleteAt(delta.source.position, delta.source.size()) trace() insertAt(delta.source.position, delta.target.lines) } is DeleteDelta -> { deleteAt(delta.source.position, delta.source.size()) } else -> { throw Exception("Can't apply: $delta") } } debug { "Applied: $delta, result:" } trace() } fun perform(): MergeResult { 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(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 merge3(source: List, a: List, b: List, showDebug: Boolean = false): MergeResult = Merge3(source, a, b).also { if (showDebug) it.logLevel = Log.Level.DEBUG }.perform()