diff --git a/.jshintrc b/.jshintrc index 5582195ae..74c9584db 100644 --- a/.jshintrc +++ b/.jshintrc @@ -13,6 +13,7 @@ "_": false, "t": false, "bootstrap": false, + "Diff3": false, "rbush": false, "JXON": false, "osmAuth": false, diff --git a/Makefile b/Makefile index e697a7544..3a36953af 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ dist/iD.js: \ js/lib/d3.typeahead.js \ js/lib/d3.curtain.js \ js/lib/d3.value.js \ + js/lib/diff3.js \ js/lib/jxon.js \ js/lib/lodash.js \ js/lib/osmauth.js \ diff --git a/index.html b/index.html index 5fc99cc5a..db41a3522 100644 --- a/index.html +++ b/index.html @@ -30,6 +30,7 @@ + diff --git a/js/lib/diff3.js b/js/lib/diff3.js new file mode 100644 index 000000000..a527d1e33 --- /dev/null +++ b/js/lib/diff3.js @@ -0,0 +1,431 @@ +// 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 + +Diff3 = (function() { + 'use strict'; + + var diff3 = { + longest_common_subsequence: function(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]; + }, + + diff_comm: function(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 = Diff3.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; + }, + + diff_patch: function(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 = Diff3.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; + }, + + strip_patch: function(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; + }, + + invert_patch: function(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; + } + }, + + patch: function (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; + }, + + diff_indices: function(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 = Diff3.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; + }, + + diff3_merge_indices: function (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 = Diff3.diff_indices(o, a); + var m2 = Diff3.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; + }, + + diff3_merge: function (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.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; + } + }; + return diff3; +})(); + +if (typeof module !== 'undefined') module.exports = Diff3; diff --git a/test/index.html b/test/index.html index 5126e198f..d2d4fa359 100644 --- a/test/index.html +++ b/test/index.html @@ -31,6 +31,7 @@ + @@ -214,6 +215,7 @@ + diff --git a/test/index_packaged.html b/test/index_packaged.html index ce200c76c..681c741d9 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -25,6 +25,7 @@ + diff --git a/test/spec/lib/diff3.js b/test/spec/lib/diff3.js new file mode 100644 index 000000000..3cf741554 --- /dev/null +++ b/test/spec/lib/diff3.js @@ -0,0 +1,89 @@ +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; + }); + +});