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