diff --git a/README.md b/README.md index 2a8b18c..07cc6ac 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,7 @@ Because `buildRevertPatch + apply` offers more flexibility over `revert` it is p * use [pack/unpack](#patch-size) with the result of `buildRevertPatch` making it ideal for storage or transport * reverse a revert (and so on...) with `{reversible: true}` * [diff](#diff) between reverts -* merge multiple reverts into one -* rebase reverts +* concat, squash, rebase multiple reverts [↑](#json8-patch) diff --git a/index.js b/index.js index 0abf2d4..dca1b57 100644 --- a/index.js +++ b/index.js @@ -31,3 +31,4 @@ module.exports.unpack = require('./lib/unpack') // Utilities module.exports.concat = require('./lib/concat') +module.exports.squash = require('./lib/squash') diff --git a/lib/squash.js b/lib/squash.js new file mode 100644 index 0000000..fc50730 --- /dev/null +++ b/lib/squash.js @@ -0,0 +1,56 @@ +'use strict' + +function wasModified(patch, path) { + for (var i = 0; i < patch.length; i++) { + var op = patch[i] + if (op.path !== path) continue + if (op.op === 'add' || op.op === 'replace' || op.op === 'remove' || op.op === 'move' || op.op === 'copy') return true + } + return false +} + +module.exports = function squash(patch) { + var squashed = [] + + var push = true + + patch.forEach(function(op) { + // if current op overrides previous operations, remove them + if (['add', 'replace', 'remove', 'move', 'copy'].indexOf(op.op) !== -1) { + squashed.forEach(function(prev, idx) { + if (prev.op === 'test') return + if (prev.path === op.path) { // same path - FIXME children/parents ? + if (wasModified(squashed, op.path)) { + squashed[idx] = undefined + } + + // the path was created in the patch, the remove operation shouldn't be included + // example + // {"path": "/foo", "op": "add", "value": "foo"}, + // {"path": "/foo", "op": "remove"} + if (op.op === 'remove') { + push = false + } + // the path was created in the patch, replace op requires the target to exist + // example + // {"path": "/foo", "op": "add", "value": "foo"}, + // {"path": "/foo", "op": "replace", "value": "bar"} + else if (op.op === 'replace') { + // push = false + op = {"path": op.path, "op": "add", "value": op.value} + } + } + }) + } + + if (push) { + squashed.push(op) + } + }) + + var foo = [] + squashed.forEach(function(op) { + if (op !== undefined) foo.push(op) + }) + return foo +} diff --git a/test/squash_same_path.js b/test/squash_same_path.js new file mode 100644 index 0000000..a2e34d0 --- /dev/null +++ b/test/squash_same_path.js @@ -0,0 +1,748 @@ +'use strict' + +import assert from 'assert' +import squash from '../lib/squash' +import apply from '../lib/apply' +import {clone} from 'json8' + +/* eslint comma-dangle: 0 */ + +function test(patch, squashed, doc, expected) { + assert.deepEqual(squash(patch), squashed) + + + if (doc) { + assert.deepEqual(apply(clone(doc), patch).doc, apply(clone(doc), squashed).doc) + } + + if (expected) { + assert.deepEqual(apply(clone(doc), patch).doc, expected) + assert.deepEqual(apply(clone(doc), squashed).doc, expected) + } +} + +/* + * add + */ +// add - add +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ], + {}, + {"foo": "bar"} +) +// add - remove +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/foo", "op": "remove"} + ], + [], + {}, + {} +) +// add - replace +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ], + {}, + {"foo": "bar"} +) +// add - move to +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"} + ], + {"bar": "lulz"}, + {"foo": "lulz"} +) +// // add - move from // FIXME this could resolve to [{"op": "add", "path": "/bar", "value": "lulz"}] +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ], + {}, + {"bar": "lulz"} +) +// add - copy to +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + {"bar": "lulz"}, + {"bar": "lulz", "foo": "lulz"} +) +// add - copy from +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ], + {}, + {"foo": "lulz", "bar": "lulz"} +) +// add - test +test( + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + [ + {"path": "/foo", "op": "add", "value": "lulz"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + {}, + {"foo": "lulz"} +) + +/* + * remove + */ +// remove - add +test( + [ + {"path": "/foo", "op": "remove"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ], + {"foo": "lulz"}, + {"foo": "bar"} +) +// // remove - remove // FIXME invalid patch ... +// test( +// [ +// {"path": "/foo", "op": "remove"}, +// {"path": "/foo", "op": "remove"} +// ], +// [ +// {"path": "/foo", "op": "remove"}, +// {"path": "/foo", "op": "remove"} +// ] +// ) +// // remove - replace // FIXME invalid patch ... +// test( +// [ +// {"path": "/foo", "op": "remove"}, +// {"path": "/foo", "op": "replace", "value": "bar"} +// ], +// [ +// {"path": "/foo", "op": "remove"}, +// {"path": "/foo", "op": "replace", "value": "bar"} +// ] +// ) +// remove - move to +test( + [ + {"path": "/foo", "op": "remove"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"} + ], + {"foo": "cat", "bar": "dog"}, + {"foo": "dog"} +) +// // remove - move from // FIXME invalid patch +// test( +// [ +// {"path": "/foo", "op": "remove"}, +// {"path": "/bar", "op": "move", "from": "/foo"} +// ], +// [ +// {"path": "/foo", "op": "move", "from": "/bar"} +// ] +// ) +// remove - copy to +test( + [ + {"path": "/foo", "op": "remove"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + {"foo": "turtle", "bar": "dog"}, + {"foo": "dog", "bar": "dog"} +) +// // remove - copy from // FIXME invalid patch +// test( +// [ +// {"path": "/foo", "op": "remove"}, +// {"path": "/bar", "op": "copy", "from": "/foo"} +// ], +// [ +// {"path": "/foo", "op": "add", "value": "lulz"}, +// {"path": "/bar", "op": "copy", "from": "/foo"} +// ] +// ) +// remove - test // FIXME invalid patch +test( + [ + {"path": "/foo", "op": "remove"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + [ + {"path": "/foo", "op": "remove"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ] +) + +/* + * replace + */ +// replace - add +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ], + {"foo": "cat"}, + {"foo": "bar"} +) +// replace - remove // FIXAAAAAAAAAAA +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/foo", "op": "remove"} + ], + [ + {"path": "/foo", "op": "remove"} + ], + {"foo": "cat"}, + {} +) +// replace - replace +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// replace - move to +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"} + ] +) +// replace - move from +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ] +) +// replace - copy to +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"} + ] +) +// replace - copy from +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ] +) +// replace - test +test( + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + [ + {"path": "/foo", "op": "replace", "value": "lulz"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ] +) + +/* + * move to + */ +// move to - add +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// move to - remove +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/foo", "op": "remove"} + ], + [] +) +// move to - replace +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// move to - move to +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"} + ] +) +// move to - move from // FIXME this could resolve to [] +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ] +) +// move to - copy to // FIXME invalid patch +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"} + ] +) +// move to - copy from // FIXME this could resolve to [{"op": "copy", "path": "/bar" from: "/foo"}] +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ] +) +// move to - test +test( + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ] +) + +/* + * move from + */ +// move from - add +test( + [ + {"path": "/bar", "op": "move", "from": "/foo"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/bar", "op": "move", "from": "/foo"}, + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// // move from - remove // FIXME invalid patch +// test( +// [ +// {"path": "/bar", "op": "move", "from": "/foo"}, +// {"path": "/foo", "op": "remove"} +// ], +// [] +// ) +// // move from - replace // FIXME invalid patch +// test( +// [ +// {"path": "/bar", "op": "move", "from": "/foo"}, +// {"path": "/foo", "op": "replace", "value": "bar"} +// ], +// [ +// {"path": "/foo", "op": "add", "value": "bar"} +// ] +// ) +// // move from - move to // FIXME invalid patch +// test( +// [ +// {"path": "/bar", "op": "move", "from": "/foo"}, +// {"path": "/foo", "op": "move", "from": "/bar"} +// ], +// [ +// {"path": "/foo", "op": "move", "from": "/bar"} +// ] +// ) +// // move from - move from // FIXME invalid patch +// test( +// [ +// {"path": "/bar", "op": "move", "from": "/foo"}, +// {"path": "/bar", "op": "move", "from": "/foo"} +// ], +// [ +// {"path": "/foo", "op": "move", "from": "/bar"}, +// {"path": "/bar", "op": "move", "from": "/foo"} +// ] +// ) +// move from - copy to // FIXME this could resolve to [{"op": "copy", "path": "/bar", "from": "/foo"}] +test( + [ + {"path": "/bar", "op": "move", "from": "/foo"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/bar", "op": "move", "from": "/foo"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ] +) +// // move from - copy from // FIXME invalid patch +// test( +// [ +// {"path": "/bar", "op": "move", "from": "/foo"}, +// {"path": "/bar", "op": "copy", "from": "/foo"} +// ], +// [ +// {"path": "/foo", "op": "move", "from": "/bar"}, +// {"path": "/bar", "op": "copy", "from": "/foo"} +// ] +// ) +// // move from - test // FIXME invalid patch +// test( +// [ +// {"path": "/bar", "op": "move", "from": "/foo"}, +// {"path": "/foo", "op": "test", "value": "lulz"} +// ], +// [ +// {"path": "/foo", "op": "move", "from": "/bar"}, +// {"path": "/foo", "op": "test", "value": "lulz"} +// ] +// ) + + +/* + * copy to + */ +// copy to - add +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// copy to - remove +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/foo", "op": "remove"} + ], + [] +) +// copy to - replace +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ], + [ + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// copy to - move to +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "move", "from": "/bar"} + ] +) +// copy to - move from // FIXME this could resolve to [{"op": "copy", "path": "/bar" from: "/foo"}] +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ] +) +// copy to - copy to +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"} + ] +) +// copy to - copy from // FIXME this could resolve to [] +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ] +) +// copy to - test +test( + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + [ + {"path": "/foo", "op": "copy", "from": "/bar"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ] +) + +/* + * copy from + */ +// copy from - add +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// copy from - remove +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "remove"} + ], + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "remove"} + ] +) +// copy from - replace +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ], + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ] +) +// copy from - move to // FIXME this could resolve to [] +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ], + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ] +) +// copy from - move from +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ], + [ + {"path": "/bar", "op": "move", "from": "/foo"} + ] +) +// copy from - copy to // FIXME this chould resolve to [{"path": "/bar", "op": "copy", "from": "/foo"}] +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ] +) +// copy from - copy from +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ], + [ + {"path": "/bar", "op": "copy", "from": "/foo"} + ] +) +// copy from - test +test( + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + [ + {"path": "/bar", "op": "copy", "from": "/foo"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ] +) + +/* + * test + */ +// test - add +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "add", "value": "bar"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "add", "value": "bar"} + ] +) +// test - remove +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "remove"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "remove"} + ] +) +// test - replace +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "replace", "value": "bar"} + ] +) +// test - move to +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "move", "from": "/bar"} + ] +) +// test - move from +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/bar", "op": "move", "from": "/foo"} + ] +) +// test - copy to +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "copy", "from": "/bar"} + ] +) +// test - copy from +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/bar", "op": "copy", "from": "/foo"} + ] +) +// test - test +test( + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ], + [ + {"path": "/foo", "op": "test", "value": "lulz"}, + {"path": "/foo", "op": "test", "value": "lulz"} + ] +)