技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

3バージョン差分が面倒だったので妥協した

先日の google_diff_match_patch で行単位の差分が出るのは良いが、3バージョンで差分を作るのが面倒だったので妥協したメモ。

まず、diff も行単位でやりたいし、面倒が嫌いなのでラップ

import scala.collection.JavaConversions._

object Diff {

  def lineDiff(base:String, changed:String):List[diff_match_patch.Diff] = {
    val diff = new diff_match_patch()
    val lines = diff.diff_linesToChars(base, changed)
    val (text1, text2, lineArray) = (lines.chars1, lines.chars2, lines.lineArray)
    val result = diff.diff_main(text1, text2, false)
    diff.diff_charsToLines(result, lineArray)
    result.toList
  }
}

で、これを利用して差分計算しようと思ったら、かなり面倒な事に気がついた。

何が面倒って、結果をどこまで信用していいのか分からない所。

Subversion とかの自動解決で痛い目を見てれば分かるが、少なくともコンフリが起こった際の削除は危険すぎる。

  • 変更元
  • 現在の最新
  • 今回の更新内容

  • 三者が居る所で、「現在の最新」<--> 「今回の更新内容」だと、欠損が出るのは明らか。(現在の最新、で追加した分が「今回の…」で削除差分になる可能性が高い)

  • でも考えてみれば、その差分というのは、「変更元」<-->「現在の最新」で追加された差分であるわけだ

で、行単位の操作だというなら、更新差分は「削除差分」+「追加差分」で扱われるので、「現在の最新」<--> 「今回の更新内容」で削除された差分が「変更元」<-->「現在の最新」で追加されている差分である場合、コンフリクト差分があると見なせる。

…筈?

package models

import utils.models.{LineOperation, Diff, diff_match_patch}
import utils.models.diff_match_patch.Operation

case class Text(
                title:String,
                text:String)

object Differ {

  case class LineDiff(operation: LineOperation, text: String) {
    override def toString() = {
      operation match {
        case LineOperation.CONFLICTED_AFTER => s"---------¥n+ $text ¥n---------¥n"
        case LineOperation.CONFLICTED_BEFORE => s"---------¥n- $text ¥n---------¥n"
        case LineOperation.DELETED => s"¥n---------¥n- $text ¥n---------¥n"
        case LineOperation.MERGED => text
      }
    }
  }

  def difference(base:Text, latest:Text, update:Text) = {
    // 最新と更新の差分
    val diffLatest = Diff.lineDiff(latest.text, update.text)
    // 更新元と最新の差分
    val diffOld    = Diff.lineDiff(base.text, latest.text)

    // 最新の変更で削除された行が過去追加された行だったらコンフリと見なす
    val merged     = diffLatest.foldLeft(collection.mutable.ListBuffer.empty[LineDiff]) { case (merged, line) =>
      line.operation match {
        case Operation.EQUAL  => merged += LineDiff(LineOperation.MERGED, line.text)
        case Operation.DELETE => {
          val filterValue = line.text.trim
          // 過去の更新で insert されてれば競合
          val conflicted = diffOld.find(p => {
            p.operation == Operation.INSERT && p.text.trim == filterValue
          }) match {
            case Some(oldLine) => LineDiff(LineOperation.CONFLICTED_BEFORE, oldLine.text) // conflicted.
            case None          => LineDiff(LineOperation.DELETED, line.text)
          }
          merged += conflicted
        }
        case Operation.INSERT => merged += LineDiff(LineOperation.CONFLICTED_AFTER, line.text)
      }
    }
    merged.toList
  }
}

という事で洗ってみたが、多分 p.text.trim == filterValue の判定が間違ってそうな悪寒。

contain な関係もある筈なので、結局の所コンフリが起きた時点で一切消さずに、差分を表示した方が良さそうという結論に至る。

そりゃそっかと納得。