From c40f242b825c4a721db1ff417ad6e9c3cdace073 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 24 Oct 2017 15:38:52 -0400 Subject: [PATCH] Depend on bhousel/node-diff3 which includes fix for hunk sorting bug (closes #3544, closes #3058) --- modules/actions/merge_remote_changes.js | 6 +- modules/util/diff3.js | 421 ------------------------ package.json | 1 + test/index.html | 1 - test/spec/lib/diff3.js | 90 ----- 5 files changed, 4 insertions(+), 515 deletions(-) delete mode 100644 modules/util/diff3.js delete mode 100644 test/spec/lib/diff3.js diff --git a/modules/actions/merge_remote_changes.js b/modules/actions/merge_remote_changes.js index b842f3322..71412bf00 100644 --- a/modules/actions/merge_remote_changes.js +++ b/modules/actions/merge_remote_changes.js @@ -9,10 +9,10 @@ import _union from 'lodash-es/union'; import _uniq from 'lodash-es/uniq'; import _without from 'lodash-es/without'; +import { diff3Merge } from 'node-diff3'; import { t } from '../util/locale'; import { actionDeleteMultiple } from './delete_multiple'; import { osmEntity } from '../osm'; -import { diff3_merge } from '../util/diff3'; import { dataDiscarded } from '../../data'; @@ -57,7 +57,7 @@ export function actionMergeRemoteChanges(id, localGraph, remoteGraph, formatUser a = target.nodes || [], b = remote.nodes || [], nodes = [], - hunks = diff3_merge(a, o, b, true); + hunks = diff3Merge(a, o, b, true); for (var i = 0; i < hunks.length; i++) { var hunk = hunks[i]; @@ -65,7 +65,7 @@ export function actionMergeRemoteChanges(id, localGraph, remoteGraph, formatUser nodes.push.apply(nodes, hunk.ok); } else { // for all conflicts, we can assume c.a !== c.b - // because `diff3_merge` called with `true` option to exclude false conflicts.. + // because `diff3Merge` called with `true` option to exclude false conflicts.. var c = hunk.conflict; if (_isEqual(c.o, c.a)) { // only changed remotely nodes.push.apply(nodes, c.b); diff --git a/modules/util/diff3.js b/modules/util/diff3.js deleted file mode 100644 index d36371a14..000000000 --- a/modules/util/diff3.js +++ /dev/null @@ -1,421 +0,0 @@ -// Copyright (c) 2006, 2008 Tony Garnock-Jones -// Copyright (c) 2006, 2008 LShift Ltd. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation files -// (the "Software"), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of the Software, -// and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -// source: https://bitbucket.org/lshift/synchrotron/src -export function longest_common_subsequence(file1, file2) { - /* Text diff algorithm following Hunt and McIlroy 1976. - * J. W. Hunt and M. D. McIlroy, An algorithm for differential file - * comparison, Bell Telephone Laboratories CSTR #41 (1976) - * http://www.cs.dartmouth.edu/~doug/ - * - * Expects two arrays of strings. - */ - var equivalenceClasses; - var file2indices; - var newCandidate; - var candidates; - var line; - var c, i, j, jX, r, s; - - equivalenceClasses = {}; - for (j = 0; j < file2.length; j++) { - line = file2[j]; - if (equivalenceClasses[line]) { - equivalenceClasses[line].push(j); - } else { - equivalenceClasses[line] = [j]; - } - } - - candidates = [{file1index: -1, - file2index: -1, - chain: null}]; - - for (i = 0; i < file1.length; i++) { - line = file1[i]; - file2indices = equivalenceClasses[line] || []; - - r = 0; - c = candidates[0]; - - for (jX = 0; jX < file2indices.length; jX++) { - j = file2indices[jX]; - - for (s = 0; s < candidates.length; s++) { - if ((candidates[s].file2index < j) && - ((s === candidates.length - 1) || - (candidates[s + 1].file2index > j))) - break; - } - - if (s < candidates.length) { - newCandidate = {file1index: i, - file2index: j, - chain: candidates[s]}; - if (r === candidates.length) { - candidates.push(c); - } else { - candidates[r] = c; - } - r = s + 1; - c = newCandidate; - if (r === candidates.length) { - break; // no point in examining further (j)s - } - } - } - - candidates[r] = c; - } - - // At this point, we know the LCS: it's in the reverse of the - // linked-list through .chain of - // candidates[candidates.length - 1]. - - return candidates[candidates.length - 1]; -} - -export function diff_comm(file1, file2) { - // We apply the LCS to build a "comm"-style picture of the - // differences between file1 and file2. - - var result = []; - var tail1 = file1.length; - var tail2 = file2.length; - var common = {common: []}; - - function processCommon() { - if (common.common.length) { - common.common.reverse(); - result.push(common); - common = {common: []}; - } - } - - for (var candidate = longest_common_subsequence(file1, file2); - candidate !== null; - candidate = candidate.chain) - { - var different = {file1: [], file2: []}; - - while (--tail1 > candidate.file1index) { - different.file1.push(file1[tail1]); - } - - while (--tail2 > candidate.file2index) { - different.file2.push(file2[tail2]); - } - - if (different.file1.length || different.file2.length) { - processCommon(); - different.file1.reverse(); - different.file2.reverse(); - result.push(different); - } - - if (tail1 >= 0) { - common.common.push(file1[tail1]); - } - } - - processCommon(); - - result.reverse(); - return result; -} - -export function diff_patch(file1, file2) { - // We apply the LCD to build a JSON representation of a - // diff(1)-style patch. - - var result = []; - var tail1 = file1.length; - var tail2 = file2.length; - - function chunkDescription(file, offset, length) { - var chunk = []; - for (var i = 0; i < length; i++) { - chunk.push(file[offset + i]); - } - return {offset: offset, - length: length, - chunk: chunk}; - } - - for (var candidate = longest_common_subsequence(file1, file2); - candidate !== null; - candidate = candidate.chain) - { - var mismatchLength1 = tail1 - candidate.file1index - 1; - var mismatchLength2 = tail2 - candidate.file2index - 1; - tail1 = candidate.file1index; - tail2 = candidate.file2index; - - if (mismatchLength1 || mismatchLength2) { - result.push({file1: chunkDescription(file1, - candidate.file1index + 1, - mismatchLength1), - file2: chunkDescription(file2, - candidate.file2index + 1, - mismatchLength2)}); - } - } - - result.reverse(); - return result; -} - -export function strip_patch(patch) { - // Takes the output of Diff3.diff_patch(), and removes - // information from it. It can still be used by patch(), - // below, but can no longer be inverted. - var newpatch = []; - for (var i = 0; i < patch.length; i++) { - var chunk = patch[i]; - newpatch.push({file1: {offset: chunk.file1.offset, - length: chunk.file1.length}, - file2: {chunk: chunk.file2.chunk}}); - } - return newpatch; -} - -export function invert_patch (patch) { - // Takes the output of Diff3.diff_patch(), and inverts the - // sense of it, so that it can be applied to file2 to give - // file1 rather than the other way around. - - for (var i = 0; i < patch.length; i++) { - var chunk = patch[i]; - var tmp = chunk.file1; - chunk.file1 = chunk.file2; - chunk.file2 = tmp; - } -} - -export function patch (file, patch) { - // Applies a patch to a file. - // - // Given file1 and file2, Diff3.patch(file1, - // Diff3.diff_patch(file1, file2)) should give file2. - - var result = []; - var commonOffset = 0; - - function copyCommon(targetOffset) { - while (commonOffset < targetOffset) { - result.push(file[commonOffset]); - commonOffset++; - } - } - - for (var chunkIndex = 0; chunkIndex < patch.length; chunkIndex++) { - var chunk = patch[chunkIndex]; - copyCommon(chunk.file1.offset); - for (var lineIndex = 0; lineIndex < chunk.file2.chunk.length; lineIndex++) { - result.push(chunk.file2.chunk[lineIndex]); - } - commonOffset += chunk.file1.length; - } - - copyCommon(file.length); - return result; -} - -export function diff_indices(file1, file2) { - // We apply the LCS to give a simple representation of the - // offsets and lengths of mismatched chunks in the input - // files. This is used by diff3_merge_indices below. - - var result = []; - var tail1 = file1.length; - var tail2 = file2.length; - - for (var candidate = longest_common_subsequence(file1, file2); - candidate !== null; - candidate = candidate.chain) - { - var mismatchLength1 = tail1 - candidate.file1index - 1; - var mismatchLength2 = tail2 - candidate.file2index - 1; - tail1 = candidate.file1index; - tail2 = candidate.file2index; - - if (mismatchLength1 || mismatchLength2) { - result.push({file1: [tail1 + 1, mismatchLength1], - file2: [tail2 + 1, mismatchLength2]}); - } - } - - result.reverse(); - return result; -} - -export function diff3_merge_indices (a, o, b) { - // Given three files, A, O, and B, where both A and B are - // independently derived from O, returns a fairly complicated - // internal representation of merge decisions it's taken. The - // interested reader may wish to consult - // - // Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce. "A - // Formal Investigation of Diff3." In Arvind and Prasad, - // editors, Foundations of Software Technology and Theoretical - // Computer Science (FSTTCS), December 2007. - // - // (http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf) - var i; - - var m1 = diff_indices(o, a); - var m2 = diff_indices(o, b); - - var hunks = []; - function addHunk(h, side) { - hunks.push([h.file1[0], side, h.file1[1], h.file2[0], h.file2[1]]); - } - for (i = 0; i < m1.length; i++) { addHunk(m1[i], 0); } - for (i = 0; i < m2.length; i++) { addHunk(m2[i], 2); } - hunks.sort(); - - var result = []; - var commonOffset = 0; - function copyCommon(targetOffset) { - if (targetOffset > commonOffset) { - result.push([1, commonOffset, targetOffset - commonOffset]); - commonOffset = targetOffset; - } - } - - for (var hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) { - var firstHunkIndex = hunkIndex; - var hunk = hunks[hunkIndex]; - var regionLhs = hunk[0]; - var regionRhs = regionLhs + hunk[2]; - while (hunkIndex < hunks.length - 1) { - var maybeOverlapping = hunks[hunkIndex + 1]; - var maybeLhs = maybeOverlapping[0]; - if (maybeLhs > regionRhs) break; - regionRhs = maybeLhs + maybeOverlapping[2]; - hunkIndex++; - } - - copyCommon(regionLhs); - if (firstHunkIndex === hunkIndex) { - // The "overlap" was only one hunk long, meaning that - // there's no conflict here. Either a and o were the - // same, or b and o were the same. - if (hunk[4] > 0) { - result.push([hunk[1], hunk[3], hunk[4]]); - } - } else { - // A proper conflict. Determine the extents of the - // regions involved from a, o and b. Effectively merge - // all the hunks on the left into one giant hunk, and - // do the same for the right; then, correct for skew - // in the regions of o that each side changed, and - // report appropriate spans for the three sides. - var regions = { - 0: [a.length, -1, o.length, -1], - 2: [b.length, -1, o.length, -1] - }; - for (i = firstHunkIndex; i <= hunkIndex; i++) { - hunk = hunks[i]; - var side = hunk[1]; - var r = regions[side]; - var oLhs = hunk[0]; - var oRhs = oLhs + hunk[2]; - var abLhs = hunk[3]; - var abRhs = abLhs + hunk[4]; - r[0] = Math.min(abLhs, r[0]); - r[1] = Math.max(abRhs, r[1]); - r[2] = Math.min(oLhs, r[2]); - r[3] = Math.max(oRhs, r[3]); - } - var aLhs = regions[0][0] + (regionLhs - regions[0][2]); - var aRhs = regions[0][1] + (regionRhs - regions[0][3]); - var bLhs = regions[2][0] + (regionLhs - regions[2][2]); - var bRhs = regions[2][1] + (regionRhs - regions[2][3]); - result.push([-1, - aLhs, aRhs - aLhs, - regionLhs, regionRhs - regionLhs, - bLhs, bRhs - bLhs]); - } - commonOffset = regionRhs; - } - - copyCommon(o.length); - return result; -} - -export function diff3_merge (a, o, b, excludeFalseConflicts) { - // Applies the output of Diff3.diff3_merge_indices to actually - // construct the merged file; the returned result alternates - // between "ok" and "conflict" blocks. - - var result = []; - var files = [a, o, b]; - var indices = diff3_merge_indices(a, o, b); - - var okLines = []; - function flushOk() { - if (okLines.length) { - result.push({ok: okLines}); - } - okLines = []; - } - function pushOk(xs) { - for (var j = 0; j < xs.length; j++) { - okLines.push(xs[j]); - } - } - - function isTrueConflict(rec) { - if (rec[2] !== rec[6]) return true; - var aoff = rec[1]; - var boff = rec[5]; - for (var j = 0; j < rec[2]; j++) { - if (a[j + aoff] !== b[j + boff]) return true; - } - return false; - } - - for (var i = 0; i < indices.length; i++) { - var x = indices[i]; - var side = x[0]; - if (side === -1) { - if (excludeFalseConflicts && !isTrueConflict(x)) { - pushOk(files[0].slice(x[1], x[1] + x[2])); - } else { - flushOk(); - result.push({conflict: {a: a.slice(x[1], x[1] + x[2]), - aIndex: x[1], - o: o.slice(x[3], x[3] + x[4]), - oIndex: x[3], - b: b.slice(x[5], x[5] + x[6]), - bIndex: x[5]}}); - } - } else { - pushOk(files[side].slice(x[1], x[1] + x[2])); - } - } - - flushOk(); - return result; -} diff --git a/package.json b/package.json index 57b4e1727..0803814e9 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "diacritics": "1.3.0", "lodash-es": "4.17.4", "marked": "0.3.6", + "node-diff3": "bhousel/node-diff3.git#v0.1.0", "osm-auth": "1.0.2", "rbush": "2.0.1", "wmf-sitematrix": "0.1.4" diff --git a/test/index.html b/test/index.html index 4d4ca1b70..c391ca88e 100644 --- a/test/index.html +++ b/test/index.html @@ -77,7 +77,6 @@ - diff --git a/test/spec/lib/diff3.js b/test/spec/lib/diff3.js deleted file mode 100644 index 588b5bbdf..000000000 --- a/test/spec/lib/diff3.js +++ /dev/null @@ -1,90 +0,0 @@ -/* global Diff3:false */ -describe('diff3', function() { - function split(s) { - return s ? s.split(/ /) : []; - } - - it('performs diff3 merge', function() { - var o = split('AA ZZ 00 M 99'), - a = split('AA a b c ZZ new 00 a a M 99'), - b = split('AA a d c ZZ 11 M z z 99'), - res = Diff3.diff3_merge(a, o, b); - - /* - AA - <<<<<<< a - a - b - c - ||||||| o - ======= - a - d - c - >>>>>>> b - ZZ - <<<<<<< a - new - 00 - a - a - ||||||| o - 00 - ======= - 11 - >>>>>>> b - M - z - z - 99 - */ - - expect(res[0].ok).to.eql(['AA']); - expect(res[0].conflict).to.be.undefined; - - expect(res[1].ok).to.be.undefined; - expect(res[1].conflict.o).to.eql([]); - expect(res[1].conflict.a).to.eql(['a', 'b', 'c']); - expect(res[1].conflict.b).to.eql(['a', 'd', 'c']); - - expect(res[2].ok).to.eql(['ZZ']); - expect(res[2].conflict).to.be.undefined; - - expect(res[3].ok).to.be.undefined; - expect(res[3].conflict.o).to.eql(['00']); - expect(res[3].conflict.a).to.eql(['new', '00', 'a', 'a']); - expect(res[3].conflict.b).to.eql(['11']); - - expect(res[4].ok).to.eql(['M', 'z', 'z', '99']); - expect(res[4].conflict).to.be.undefined; - }); - - it('can include false conflicts', function() { - var o = split('AA ZZ'), - a = split('AA a b c ZZ'), - b = split('AA a b c ZZ'), - res = Diff3.diff3_merge(a, o, b, false); - - expect(res[0].ok).to.eql(['AA']); - expect(res[0].conflict).to.be.undefined; - - expect(res[1].ok).to.be.undefined; - expect(res[1].conflict.o).to.eql([]); - expect(res[1].conflict.a).to.eql(['a', 'b', 'c']); - expect(res[1].conflict.b).to.eql(['a', 'b', 'c']); - - expect(res[2].ok).to.eql(['ZZ']); - expect(res[2].conflict).to.be.undefined; - }); - - it('can exclude false conflicts', function() { - var o = split('AA ZZ'), - a = split('AA a b c ZZ'), - b = split('AA a b c ZZ'), - res = Diff3.diff3_merge(a, o, b, true); - - expect(res[0].ok).to.eql(['AA', 'a', 'b', 'c', 'ZZ']); - expect(res[0].conflict).to.be.undefined; - }); - -});