blog

テキストをマージしたい(3) – 3-way-merge 簡易実装

前回、JavaDiffUtils の基本動作を確認しました。
今回は簡易な実装で3-way-merge を行なっていきます。


3-way-merge 基本方針

applyTo() は便利ですが、「両方の変更を残す」という要件は満たせません。そこで、source.lines とtarget.lines を使って、自前でマージを行います。

方針

  • 修正なし –> base の行をそのまま採用
  • 片方のみ修正 –> その修正版を採用
  • 両方修正 –> 両方の修正を採用

実装例

kotlin
fun merge(base: List<String>, local: List<String>, remote: List<String>): List<String> {
    val localPatch = DiffUtils.diff(base, local)
    val remotePatch = DiffUtils.diff(base, remote)

    // 元文章の何行目に関する修正かMap 型に変換しておく
    // 適用済み管理と速度向上が目的なのでmutable
    val localDeltasFirstPositionMap = localPatch.deltas.associateBy { it.source.position }.toMutableMap()
    val remoteDeltasFirstPositionMap = remotePatch.deltas.associateBy { it.source.position }.toMutableMap()

    val result = mutableListOf<String>()
    var index = 0
    // 文末に行を追加した場合、source.position がbase 最終行+1 となるので、base.size までループ
    while (index <= base.size) {
        val localDelta = localDeltasFirstPositionMap.remove(index)
        val remoteDelta = remoteDeltasFirstPositionMap.remove(index)

        when {
            localDelta == null && remoteDelta == null -> {
                // 修正なし
                if (index < base.size) result += base[index]
                index++
            }
            localDelta != null && remoteDelta == null -> {
                // 片方修正(local)
                result += localDelta.target.lines
                index += localDelta.source.lines.size
            }
            localDelta == null && remoteDelta != null -> {
                // 片方修正(remote)
                result += remoteDelta.target.lines
                index += remoteDelta.source.lines.size
            }
            else -> {
                // 両方修正
                result += localDelta!!.target.lines
                result += remoteDelta!!.target.lines
                index += localDelta.source.lines.size
            }
        }
    }
    return result
}

動かすとこのようになります。

val base = listOf("AAA", "BBB", "CCC")
val local = listOf("AAA", "BeB", "CCC")
val remote = listOf("AAA", "BfB", "CCC")
val result = merge(base, local, remote)
result shouldBe listOf("AAA", "BeB", "BfB", "CCC")

AAA
BeB
BfB
CCC

行単位でのマージはこれでバッチリです。
ですが、厳密にはコンフリクト時に問題があり、このロジックでは一部の文字が失われる可能性があります(詳細は次回説明します)。
一方で、コンフリクトが発生した行を文字単位に分解し、同じロジックを適用すれば目的の結果を得られそうです。

上のロジックのまま、文字単位でのマージを試してみます。

文字単位でマージ

2行目を文字単位で分解して、先ほどのロジックを適用してみます。

val base = listOf("B", "B", "B")
val local = listOf("B", "e", "B")
val remote = listOf("B", "f", "B")
val result = merge(base, local, remote)
result shouldBe listOf("B", "e", "f", "B")

BefB

期待通りの結果が得られました。
行単位でマージを試みて、コンフリクトが発生した行は文字単位でマージを行う方針が良さそうです。

次回予告

コンフリクト時の問題についてお話しします。

【広告】

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です