fs-tree-diff
Advanced tools
Comparing version 0.4.4 to 0.5.0
@@ -16,7 +16,2 @@ 'use strict'; | ||
this.mtime = mtime; | ||
// ---------------------------------------------------------------------- | ||
// optional properties | ||
this.linkDir = false; | ||
} | ||
@@ -32,6 +27,6 @@ | ||
// TODO: remove this | ||
// required methods | ||
Entry.prototype.isDirectory = function() { | ||
return Entry.isDirectory(this); | ||
}; | ||
225
lib/index.js
'use strict'; | ||
/* global Set:true */ | ||
var Set = require('fast-ordered-set'); | ||
var Tree = require('./tree'); | ||
var Entry = require('./entry'); | ||
var debug = require('debug')('fs-tree-diff:'); | ||
var util = require('./util'); | ||
var sortAndExpand = util.sortAndExpand; | ||
var validateSortedUnique = util.validateSortedUnique; | ||
@@ -17,22 +16,16 @@ var ARBITRARY_START_OF_TIME = 0; | ||
if (options._entries) { | ||
this.entries = options._entries; | ||
var entries = options.entries || []; | ||
if (options.sortAndExpand) { | ||
sortAndExpand(entries); | ||
} else { | ||
var inputs = options.entries || []; | ||
validateSortedUnique(entries); | ||
} | ||
this.entries = new Set(inputs, 'relativePath'); | ||
if (this.entries.size !== inputs.length) { | ||
var uniqInputs = new Set(); | ||
for (var i=0; i<inputs.length; ++i) { | ||
if (uniqInputs.has(inputs[i].relativePath)) { | ||
throw new Error('Duplicate Entry "' + inputs[i].relativePath + '"'); | ||
} | ||
uniqInputs.add(inputs[i].relativePath); | ||
} | ||
} | ||
} | ||
this.entries = entries; | ||
} | ||
FSTree.fromPaths = function(paths) { | ||
FSTree.fromPaths = function(paths, options) { | ||
if (typeof options !== 'object') { options = {}; } | ||
var entries = paths.map(function(path) { | ||
@@ -44,2 +37,3 @@ return new Entry(path, 0, ARBITRARY_START_OF_TIME); | ||
entries: entries, | ||
sortAndExpand: options.sortAndExpand, | ||
}); | ||
@@ -49,150 +43,119 @@ }; | ||
FSTree.fromEntries = function(entries) { | ||
FSTree.fromEntries = function(entries, options) { | ||
if (typeof options !== 'object') { options = {}; } | ||
return new FSTree({ | ||
entries: entries | ||
entries: entries, | ||
sortAndExpand: options.sortAndExpand, | ||
}); | ||
}; | ||
FSTree._fromOwnSet = function(set) { | ||
return new FSTree({ _entries: set }); | ||
}; | ||
Object.defineProperty(FSTree.prototype, 'size', { | ||
get: function() { | ||
return this.entries.size; | ||
return this.entries.length; | ||
} | ||
}); | ||
FSTree.prototype.forEach = function (fn, context) { | ||
FSTree.prototype.forEach = function(fn, context) { | ||
this.entries.forEach(fn, context); | ||
}; | ||
FSTree.prototype.difference = function(otherFSTree) { | ||
return FSTree._fromOwnSet(this.entries.difference(otherFSTree.entries)); | ||
}; | ||
FSTree.prototype.calculatePatch = function(otherFSTree, isEqual) { | ||
if (arguments.length > 1 && typeof isEqual !== 'function') { | ||
throw new TypeError('calculatePatch\'s second argument must be a function'); | ||
} | ||
FSTree.prototype.intersection = function(otherFSTree) { | ||
return FSTree._fromOwnSet(this.entries.intersection(otherFSTree.entries)); | ||
}; | ||
if (typeof isEqual !== 'function') { | ||
isEqual = FSTree.defaultIsEqual; | ||
} | ||
FSTree.prototype.calculatePatch = function (otherFSTree) { | ||
// TODO: algorithimic complexity here isn't ideal. Future work can reduce | ||
// that cost. Today, the FS IO operations outweigh the cost, even with a | ||
// naive implementation | ||
var tree = new Tree(this.entries); | ||
var ours = this.entries; | ||
var theirs = otherFSTree.entries; | ||
var operations = []; | ||
var fsRemoveTree = this.difference(otherFSTree); | ||
var fsAddTree = otherFSTree.difference(this); | ||
var i = 0; | ||
var j = 0; | ||
// TODO: removeEntries should be combined with the postOrderDepthReducer and return removeOps | ||
tree.removeEntries(fsRemoveTree.entries); | ||
var removeOps = tree.postOrderDepthReducer(reduceRemovals, []); | ||
var removals = []; | ||
// TODO: addEntries should be combined with the preOrderDepthReducer and return addOps | ||
tree.addEntries(fsAddTree.entries); | ||
var createOps = tree.preOrderDepthReducer(reduceAdditions, []); | ||
var command; | ||
var changes = this._findChanges(otherFSTree).map(function(entry) { | ||
return ['change', entry.relativePath, entry]; | ||
}); | ||
while (i < ours.length && j < theirs.length) { | ||
var x = ours[i]; | ||
var y = theirs[j]; | ||
return removeOps.concat(createOps).concat(changes); | ||
}; | ||
if (x.relativePath < y.relativePath) { | ||
// ours | ||
i++; | ||
FSTree.prototype._findChanges = function(nextTree) { | ||
var next = this.intersection(nextTree).entries.values; | ||
var previous = nextTree.intersection(this).entries.values; | ||
command = removeCommand(x); | ||
if (next.length !== previous.length) { | ||
throw new Error('EWUT'); | ||
} | ||
if (x.isDirectory()) { | ||
removals.push(command); | ||
} else { | ||
// pre-cleanup file removals should occure in-order, this ensures file | ||
// -> directory transforms work correctly | ||
operations.push(command); | ||
} | ||
var changes = []; | ||
for (var i = 0; i < next.length; i++) { | ||
if (needsUpdate(next[i], previous[i])) { | ||
changes.push(next[i]); | ||
// remove operations | ||
} else if (x.relativePath > y.relativePath) { | ||
// theirs | ||
j++; | ||
operations.push(addCommand(y)); | ||
} else { | ||
if (!isEqual(x, y)) { | ||
command = updateCommand(y); | ||
if (x.isDirectory()) { | ||
removals.push(command); | ||
} else { | ||
operations.push(command); | ||
} | ||
} | ||
// both are the same | ||
i++; j++; | ||
} | ||
} | ||
return changes; | ||
}; | ||
// cleanup ours | ||
for (; i < ours.length; i++) { | ||
removals.push(removeCommand(ours[i])); | ||
} | ||
function needsUpdate(before, after) { | ||
if (before.isDirectory() && after.isDirectory()) { | ||
return false; | ||
// cleanup theirs | ||
for (; j < theirs.length; j++) { | ||
operations.push(addCommand(theirs[j])); | ||
} | ||
var invalidate = before.size !== after.size || | ||
+before.mtime !== +after.mtime || | ||
before.mode !== after.mode; | ||
return operations.concat(removals.reverse()); | ||
}; | ||
if (invalidate) { | ||
debug('invalidation reason: \nbefore %o\n after %o', before, after); | ||
FSTree.defaultIsEqual = function defaultIsEqual(entryA, entryB) { | ||
if (entryA.isDirectory() && entryB.isDirectory()) { | ||
// ignore directory changes by default | ||
return true; | ||
} | ||
return invalidate; | ||
} | ||
var equal = entryA.size === entryB.size && | ||
+entryA.mtime === +entryB.mtime && | ||
entryA.mode === entryB.mode; | ||
function reduceAdditions(tree, acc) { | ||
var childNames = Object.keys(tree.children); | ||
if (!equal) { | ||
debug('invalidation reason: \nbefore %o\n entryB %o', entryA, entryB); | ||
} | ||
var createdChildren = childNames.reduce(function (ops, childName) { | ||
var child = tree.children[childName]; | ||
if (child.isNew) { | ||
var operation; | ||
if (child.isFile) { | ||
operation = 'create'; | ||
} else { | ||
operation = (child.entry.linkDir) ? 'linkdir' : 'mkdir'; | ||
} | ||
return equal; | ||
}; | ||
child.isNew = false; | ||
ops.push([ | ||
operation, | ||
tree.pathForChild(childName), | ||
child.entry | ||
]); | ||
} | ||
function addCommand(entry) { | ||
return [entry.isDirectory() ? 'mkdir' : 'create', entry.relativePath, entry]; | ||
} | ||
return ops; | ||
}, []); | ||
return acc.concat(createdChildren); | ||
function removeCommand(entry) { | ||
return [entry.isDirectory() ? 'rmdir' : 'unlink', entry.relativePath, entry]; | ||
} | ||
function reduceRemovals(tree, acc) { | ||
var childNames = Object.keys(tree.children); | ||
var removeChildrenOps = childNames.reduce(function (ops, childName) { | ||
var child = tree.children[childName]; | ||
if (child.operation === Tree.RMToken) { | ||
var operation; | ||
if (child.isFile) { | ||
operation = 'unlink'; | ||
} else { | ||
operation = (child.entry.linkDir) ? 'unlinkdir' : 'rmdir'; | ||
} | ||
ops.push([ | ||
operation, | ||
tree.pathForChild(childName), | ||
undefined | ||
]); | ||
delete tree.children[childName]; | ||
} | ||
return ops; | ||
}, []); | ||
var isRoot = tree.path === undefined; | ||
if (isRoot) { | ||
return acc.concat(removeChildrenOps); | ||
} else if (removeChildrenOps.length === childNames.length) { | ||
tree.operation = Tree.RMToken; | ||
return acc.concat(removeChildrenOps); | ||
} else { | ||
return acc.concat(removeChildrenOps); | ||
} | ||
function updateCommand(entry) { | ||
return ['change', entry.relativePath, entry]; | ||
} |
126
lib/util.js
'use strict'; | ||
function byRelativePath(entry) { | ||
return entry.relativePath; | ||
var Entry = require('./entry'); | ||
function validateSortedUnique(entries) { | ||
for (var i = 1; i < entries.length; i++) { | ||
var previous = entries[i - 1].relativePath; | ||
var current = entries[i].relativePath; | ||
if (previous < current) { | ||
continue; | ||
} else { | ||
throw new Error('expected entries[' + (i -1) + ']: `' + previous + | ||
'` to be < entries[' + i + ']: `' + current + '`, but was not. Ensure your input is sorted and has no duplicate paths'); | ||
} | ||
} | ||
} | ||
function chomp(string, character) { | ||
if (string.charAt(string.length-1) === character) { | ||
return string.substring(0, string.length-1); | ||
} else { | ||
return string; | ||
function commonPrefix(a, b, term) { | ||
var max = Math.min(a.length, b.length); | ||
var end = -1; | ||
for(var i = 0; i < max; ++i) { | ||
if (a[i] !== b[i]) { | ||
break; | ||
} else if (a[i] === term) { | ||
end = i; | ||
} | ||
} | ||
return a.substr(0, end + 1); | ||
} | ||
module.exports.byRelativePath = byRelativePath; | ||
module.exports.chomp = chomp; | ||
function basename(entry) { | ||
var path = entry.relativePath; | ||
var end = path.length - 2; | ||
for(var i = end; i >= 0; --i) { | ||
if (path[i] === '/') { | ||
return path.substr(0, i + 1); | ||
} | ||
} | ||
return ''; | ||
} | ||
function computeImpliedEntries(basePath, relativePath) { | ||
var rv = []; | ||
for (var i=0; i<relativePath.length; ++i) { | ||
if (relativePath[i] === '/') { | ||
var path = basePath + relativePath.substr(0, i + 1); | ||
rv.push(new Entry(path, 0, 0)); | ||
} | ||
} | ||
return rv; | ||
} | ||
function compareByRelativePath(entryA, entryB) { | ||
var pathA = entryA.relativePath; | ||
var pathB = entryB.relativePath; | ||
if (pathA < pathB) { | ||
return -1; | ||
} else if (pathA > pathB) { | ||
return 1; | ||
} | ||
return 0; | ||
} | ||
function sortAndExpand(entries) { | ||
entries.sort(compareByRelativePath); | ||
var path = ''; | ||
for (var i=0; i<entries.length; ++i) { | ||
var entry = entries[i]; | ||
// update our path eg | ||
// path = a/b/c/d/ | ||
// entry = a/b/q/r/s/ | ||
// path' = a/b/ | ||
path = commonPrefix(path, entry.relativePath, '/'); | ||
// a/b/ -> a/ | ||
// a/b -> a/ | ||
var base = basename(entry); | ||
// base - path | ||
var entryBaseSansCommon = base.substr(path.length); | ||
// determine what intermediate directories are missing eg | ||
// path = a/b/ | ||
// entryBaseSansCommon = c/d/e/ | ||
// impliedEntries = [a/b/c/, a/b/c/d/, a/b/c/d/e/] | ||
var impliedEntries = computeImpliedEntries(path, entryBaseSansCommon); | ||
// actually add our implied entries to entries | ||
if (impliedEntries.length > 0) { | ||
entries.splice.apply(entries, [i, 0].concat(impliedEntries)); | ||
i += impliedEntries.length; | ||
} | ||
// update path. Now that we've created all the intermediate directories, we | ||
// don't need to recreate them for subsequent entries. | ||
if (entry.isDirectory()) { | ||
path = entry.relativePath; | ||
} else { | ||
path = base; | ||
} | ||
} | ||
return entries; | ||
} | ||
module.exports = { | ||
validateSortedUnique: validateSortedUnique, | ||
sortAndExpand: sortAndExpand, | ||
// exported for testing | ||
_commonPrefix: commonPrefix, | ||
_basename: basename, | ||
_computeImpliedEntries: computeImpliedEntries, | ||
}; |
{ | ||
"name": "fs-tree-diff", | ||
"version": "0.4.4", | ||
"version": "0.5.0", | ||
"description": "Backs out file tree changes", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
153
README.md
@@ -9,9 +9,7 @@ # fs-tree-diff [![Build Status](https://travis-ci.org/stefanpenner/fs-tree-diff.svg)](https://travis-ci.org/stefanpenner/fs-tree-diff) | ||
* `rmdir` – remove the specified folder | ||
* `unlinkdir` - remove the specified folder symlink | ||
* `mkdir` – create the specified folder | ||
* `linkdir` - symlink the specified folder | ||
* `create` – create the specified file | ||
* `change` – update the specified file to reflect changes | ||
The operations choosen aim to minimize the amount of IO required to apply a given patch. | ||
The operations chosen aim to minimize the amount of IO required to apply a given patch. | ||
For example, a naive `rm -rf` of a directory tree is actually quite costly, as child directories | ||
@@ -21,4 +19,6 @@ must be recursively traversed, entries stated.. etc, all to figure out what first must be deleted. | ||
The operations will also be provided in the correct order. So when deleting a large tree, unlink | ||
and rmdir operations will be provided depthFirst. Allowing us to safely replay the operations without having to first confirm the FS is as we expected. | ||
The operations will also be provided in a correct order, allowing us to safely | ||
replay operations without having to first confirm the FS is as we expect. For | ||
example, `unlink`s for files will occur before a `rmdir` of those files' parent | ||
dir. Although the ordering will be safe, a specific order is not guaranteed. | ||
@@ -33,5 +33,5 @@ A simple example: | ||
var next = FSTree.fromPaths({ | ||
var next = FSTree.fromPaths([ | ||
'b.js' | ||
}); | ||
]); | ||
@@ -50,24 +50,33 @@ current.calculatePatch(next) === [ | ||
'a.js', | ||
'b/', | ||
'b/f.js' | ||
]); | ||
var next = FSTree.fromPaths({ | ||
var next = FSTree.fromPaths([ | ||
'b.js', | ||
'b/c/d.js' | ||
'b/', | ||
'b/c/', | ||
'b/c/d.js', | ||
'b/e.js' | ||
}); | ||
]); | ||
current.calculatePatch(next) === [ | ||
['unlink', 'a.js'], | ||
['unlink', 'b/f.js'], | ||
['create', 'b.js'], | ||
['mkdir', 'b/c'], | ||
['create', 'b/c/d.js'], | ||
['create', 'b/e.js'] | ||
]; | ||
['unlink', 'a.js', entryA], | ||
['create', 'b.js', entryB], | ||
['mkdir', 'b/c', entryBC], | ||
['create', 'b/c/d.js', entryBCD], | ||
['create', 'b/e.js', entryBE] | ||
['unlink', 'b/f.js', entryBF], | ||
] | ||
``` | ||
Now, the above examples do not demonstrate `update` operations. This is because when providing only paths, we do not have sufficient information to check if one entry is merely different from another with the same relativePath. | ||
Now, the above examples do not demonstrate `update` operations. This is because | ||
when providing only paths, we do not have sufficient information to check if | ||
one entry is merely different from another with the same relativePath. | ||
For this, FSTree supports more complex input structure. To demonstrate, We will use the [walk-sync](https://github.com/joliss/node-walk-sync) module. Which provides higher fidelity input, allowing FSTree to also detect changes. More on what an [entry from walkSync.entries is](https://github.com/joliss/node-walk-sync#entries) | ||
For this, FSTree supports more complex input structure. To demonstrate, We will | ||
use the [walk-sync](https://github.com/joliss/node-walk-sync) module. Which | ||
provides higher fidelity input, allowing FSTree to also detect changes. More on | ||
what an [entry from walkSync.entries | ||
is](https://github.com/joliss/node-walk-sync#entries) | ||
@@ -91,7 +100,109 @@ ```js | ||
current.calculatePatch(next) === [ | ||
['update', 'foo.js'], // mtime + size changed, so this input is stale and needs updating. | ||
['create', 'baz.js'] // new file, so we should create it | ||
['update', 'foo.js', entryFoo], // mtime + size changed, so this input is stale and needs updating. | ||
['create', 'baz.js', entryBaz] // new file, so we should create it | ||
/* bar stays the same and is left inert*/ | ||
]; | ||
``` | ||
The entry objects provided depend on the operation. For `rmdir` and `unlink` | ||
operations, the current entry is provided. For `mkdir`, `change` and `create` | ||
operations the new entry is provided. | ||
## API | ||
The public API is: | ||
- `FSTree.fromPaths` initialize a tree from an array of string paths. | ||
- `FSTree.fromEntries` initialize a tree from an object containing an `entries` | ||
property. Each entry must have the following properties (but may have more): | ||
- `relativePath` | ||
- `mode` | ||
- `size` | ||
- `mtime` | ||
- `FSTree.prototype.calculatePatch(newTree, isEqual)` calculate a patch against | ||
`newTree`. Optionally specify a custom `isEqual` (see Change Calculation). | ||
## Input | ||
`FSTree.fromPaths` and `FSTree.fromEntries` both validate their inputs. Inputs | ||
must be sorted, path-unique (ie two entries with the same `relativePath` but | ||
different `size`s would still be illegal input) and include intermediate | ||
directories. | ||
For example, the following input is **invaild** | ||
```js | ||
FSTree.fromPaths([ | ||
// => missing a/ and a/b/ | ||
'a/b/c.js' | ||
]); | ||
``` | ||
To have FSTree sort and expand (include intermediate directories) for you, add | ||
the option `sortAndExpand`). | ||
```js | ||
FStree.fromPaths([ | ||
'a/b/q/r/bar.js', | ||
'a/b/c/d/foo.js', | ||
], { sortAndExpand: true }); | ||
// The above is equivalent to | ||
FSTree.fromPaths([ | ||
'a/', | ||
'a/b/', | ||
'a/b/c/', | ||
'a/b/c/d/', | ||
'a/b/c/d/foo.js', | ||
'a/b/q/', | ||
'a/b/q/r/', | ||
'a/b/q/r/bar.js', | ||
]); | ||
``` | ||
## Entry | ||
`FSTree.fromEntries` requires you to supply your own `Entry` objects. Your | ||
entry objects **must** contain the following properties: | ||
- `relativePath` | ||
- `mode` | ||
- `size` | ||
- `mtime` | ||
They must also implement the following API: | ||
- `isDirectory()` `true` *iff* this entry is a directory | ||
## Change Calculation | ||
When a prior entry has a `relativePath` that matches that of a current entry, a | ||
change operation is included if the new entry is different from the previous | ||
entry. This is determined by calling `isEqual`, the optional second argument | ||
to `calculatePatch`. If no `isEqual` is provided, a default `isEqual` is used. | ||
The default `isEqual` treats directories as always equal and files as different | ||
if any of the following properties have changed. | ||
- `mode` | ||
- `size` | ||
- `mtime` | ||
User specified `isEqual` will often want to use the default `isEqual`, so it is exported on `FSTree`. | ||
Example | ||
```js | ||
var defaultIsEqual = FSTtreeDiff.isEqual; | ||
function isEqualCheckingMeta(a, b) { | ||
return defaultIsEqual(a, b) && isMetaEqual(a, b); | ||
} | ||
function isMetaEqual(a, b) { | ||
// ... | ||
} | ||
``` | ||
@@ -7,5 +7,7 @@ 'use strict'; | ||
var context = describe; | ||
var defaultIsEqual = FSTree.defaultIsEqual; | ||
var fsTree; | ||
require('chai').config.truncateThreshold = 0; | ||
describe('FSTree', function() { | ||
@@ -32,3 +34,5 @@ function merge(x, y) { | ||
this.linkDir = options.linkDir; | ||
if (options.meta) { | ||
this.meta = options.meta; | ||
} | ||
} | ||
@@ -38,2 +42,26 @@ | ||
function metaIsEqual(a, b) { | ||
var aMeta = a.meta; | ||
var bMeta = b.meta; | ||
var metaKeys = aMeta ? Object.keys(aMeta) : []; | ||
var otherMetaKeys = bMeta ? Object.keys(bMeta) : []; | ||
if (metaKeys.length !== Object.keys(otherMetaKeys).length) { | ||
return false; | ||
} else { | ||
for (var i=0; i<metaKeys.length; ++i) { | ||
if (aMeta[metaKeys[i]] !== bMeta[metaKeys[i]]) { | ||
return false; | ||
} | ||
} | ||
} | ||
return true; | ||
} | ||
function userProvidedIsEqual(a, b) { | ||
return defaultIsEqual(a, b) && metaIsEqual(a, b); | ||
} | ||
function file(relativePath, options) { | ||
@@ -56,6 +84,12 @@ return entry(merge({ relativePath: relativePath }, options)); | ||
mtime: options.mtime || 0, | ||
linkDir: options.linkDir || false, | ||
meta: options.meta, | ||
}); | ||
} | ||
function by(property) { | ||
return function pluckProperty(item) { | ||
return item[property]; | ||
}; | ||
} | ||
it('can be instantiated', function() { | ||
@@ -71,2 +105,77 @@ expect(new FSTree()).to.be.an.instanceOf(FSTree); | ||
describe('input validation', function() { | ||
it('throws on duplicate', function() { | ||
expect(function() { | ||
FSTree.fromPaths([ | ||
'a', | ||
'a', | ||
]); | ||
}).to.throw('expected entries[0]: `a` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); | ||
}); | ||
it('throws on unsorted', function() { | ||
expect(function() { | ||
FSTree.fromPaths([ | ||
'b', | ||
'a', | ||
]); | ||
}).to.throw('expected entries[0]: `b` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); | ||
}); | ||
}); | ||
describe('options', function() { | ||
describe('sortAndExpand', function() { | ||
it('sorts input entries', function() { | ||
fsTree = FSTree.fromPaths([ | ||
'foo/', | ||
'foo/a.js', | ||
'bar/', | ||
'bar/b.js', | ||
], { sortAndExpand: true }); | ||
expect(fsTree.entries.map(by('relativePath'))).to.deep.equal([ | ||
'bar/', | ||
'bar/b.js', | ||
'foo/', | ||
'foo/a.js', | ||
]); | ||
}); | ||
it('expands intermediate directories implied by input entries', function() { | ||
fsTree = FSTree.fromPaths([ | ||
'a/b/q/r/bar.js', | ||
'a/b/c/d/foo.js', | ||
], { sortAndExpand: true }); | ||
expect(fsTree.entries).to.deep.equal([ | ||
directory('a/'), | ||
directory('a/b/'), | ||
directory('a/b/c/'), | ||
directory('a/b/c/d/'), | ||
file('a/b/c/d/foo.js'), | ||
directory('a/b/q/'), | ||
directory('a/b/q/r/'), | ||
file('a/b/q/r/bar.js'), | ||
]); | ||
}); | ||
it('does not mutate its input', function() { | ||
var paths = [ | ||
'foo/', | ||
'foo/a.js', | ||
'bar/', | ||
'bar/b.js', | ||
]; | ||
fsTree = FSTree.fromPaths(paths, { sortAndExpand: true }); | ||
expect(paths).to.deep.equal([ | ||
'foo/', | ||
'foo/a.js', | ||
'bar/', | ||
'bar/b.js', | ||
]); | ||
}); | ||
}); | ||
}); | ||
it('creates trees from paths', function() { | ||
@@ -77,2 +186,3 @@ var result; | ||
'a.js', | ||
'foo/', | ||
'foo/a.js', | ||
@@ -84,2 +194,3 @@ ]); | ||
'a.js', | ||
'foo/', | ||
'foo/b.js', | ||
@@ -90,7 +201,3 @@ ]) | ||
expect(result).to.deep.equal([ | ||
['unlink', 'foo/a.js', undefined], | ||
// This no-op is not fundamental: a future iteration could reasonably | ||
// optimize it away | ||
['rmdir', 'foo', undefined], | ||
['mkdir', 'foo', directory('foo')], | ||
['unlink', 'foo/a.js', file('foo/a.js')], | ||
['create', 'foo/b.js', file('foo/b.js')] | ||
@@ -102,2 +209,23 @@ ]); | ||
describe('.fromEntries', function() { | ||
describe('input validation', function() { | ||
it('throws on duplicate', function() { | ||
expect(function() { | ||
FSTree.fromEntries([ | ||
file('a', { size: 1, mtime: 1 }), | ||
file('a', { size: 1, mtime: 2 }), | ||
]); | ||
}).to.throw('expected entries[0]: `a` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); | ||
}); | ||
it('throws on unsorted', function() { | ||
expect(function() { | ||
FSTree.fromEntries([ | ||
file('b'), | ||
file('a'), | ||
]); | ||
}).to.throw('expected entries[0]: `b` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); | ||
}); | ||
}); | ||
it('creates empty trees', function() { | ||
@@ -111,4 +239,4 @@ fsTree = FSTree.fromEntries([ ]); | ||
file('a/b.js', { size: 1, mtime: 1 }), | ||
file('a/c.js', { size: 1, mtime: 1 }), | ||
file('c/d.js', { size: 1, mtime: 1 }), | ||
file('a/c.js', { size: 1, mtime: 1 }) | ||
]); | ||
@@ -120,4 +248,4 @@ | ||
file('a/b.js', { size: 1, mtime: 2 }), | ||
file('a/c.js', { size: 1, mtime: 1 }), | ||
file('c/d.js', { size: 1, mtime: 1 }), | ||
file('a/c.js', { size: 1, mtime: 1 }) | ||
])); | ||
@@ -132,2 +260,8 @@ | ||
describe('#calculatePatch', function() { | ||
context('input validation', function() { | ||
expect(function() { | ||
FSTree.fromPaths([]).calculatePatch(FSTree.fromPaths([]), ''); | ||
}).to.throw(TypeError, 'calculatePatch\'s second argument must be a function'); | ||
}); | ||
context('from an empty tree', function() { | ||
@@ -147,19 +281,11 @@ beforeEach( function() { | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([ | ||
'bar/', | ||
'bar/baz.js', | ||
'foo.js', | ||
]))).to.deep.equal([ | ||
['mkdir', 'bar', directory('bar')], | ||
['mkdir', 'bar/', directory('bar/')], | ||
['create', 'bar/baz.js', file('bar/baz.js')], | ||
['create', 'foo.js', file('foo.js')], | ||
['create', 'bar/baz.js', file('bar/baz.js')], | ||
]); | ||
}); | ||
it('throws an error for duplicate paths', function() { | ||
expect(function () { | ||
fsTree.calculatePatch(FSTree.fromEntries([ | ||
file('a/foo.js', { size: 1, mtime: 1 }), | ||
file('a/foo.js', { size: 1, mtime: 2 }), | ||
])); | ||
}).to.throw('Duplicate Entry "a/foo.js"'); | ||
}); | ||
}); | ||
@@ -171,2 +297,3 @@ }); | ||
fsTree = FSTree.fromPaths([ | ||
'bar/', | ||
'bar/baz.js', | ||
@@ -180,5 +307,5 @@ 'foo.js', | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([]))).to.deep.equal([ | ||
['unlink', 'bar/baz.js', undefined], | ||
['rmdir', 'bar', undefined], | ||
['unlink', 'foo.js', undefined] | ||
['unlink', 'foo.js', file('foo.js')], | ||
['unlink', 'bar/baz.js', file('bar/baz.js')], | ||
['rmdir', 'bar/', directory('bar/')], | ||
]); | ||
@@ -194,5 +321,7 @@ }); | ||
entries: [ | ||
file('a/b.js', { size: 1, mtime: 1, mode: '0o666' }), | ||
file('c/d.js', { size: 1, mtime: 1, mode: '0o666' }), | ||
file('a/c.js', { size: 1, mtime: 1, mode: '0o666' }) | ||
directory('a/'), | ||
file('a/b.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
file('a/c.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
directory('c/'), | ||
file('c/d.js', { mode: '0o666', size: 1, mtime: 1, meta: { rev: 0 } }) | ||
] | ||
@@ -202,9 +331,11 @@ }); | ||
it('should detect additions', function() { | ||
it('detects additions', function() { | ||
var result = fsTree.calculatePatch(new FSTree({ | ||
entries: [ | ||
file('a/b.js', { size: 1, mtime: 1, mode: '0o666' }), | ||
file('c/d.js', { size: 1, mtime: 1, mode: '0o666' }), | ||
file('a/c.js', { size: 1, mtime: 1, mode: '0o666' }), | ||
file('a/j.js', { size: 1, mtime: 1, mode: '0o666' }) | ||
directory('a/'), | ||
file('a/b.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
file('a/c.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
file('a/j.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
directory('c/'), | ||
file('c/d.js', { mode: '0o666', size: 1, mtime: 1, meta: { rev: 0 } }), | ||
] | ||
@@ -214,30 +345,28 @@ })); | ||
expect(result).to.deep.equal([ | ||
['create', 'a/j.js', file('a/j.js', { size: 1, mtime: 1, mode: '0o666'})] | ||
['create', 'a/j.js', file('a/j.js', { mode: '0o666', size: 1, mtime: 1 })] | ||
]); | ||
}); | ||
it('should detect removals', function() { | ||
var e = entry({ | ||
relativePath: 'a/b.js', | ||
mode: '0o666', | ||
size: 1, | ||
mtime: 1 | ||
}); | ||
it('detects removals', function() { | ||
var result = fsTree.calculatePatch(new FSTree({ | ||
entries: [e] | ||
entries: [ | ||
directory('a/'), | ||
entry({ relativePath: 'a/b.js', mode: '0o666', size: 1, mtime: 1 }) | ||
] | ||
})); | ||
expect(result).to.deep.equal([ | ||
['unlink', 'a/c.js', undefined], | ||
['unlink', 'c/d.js', undefined], | ||
['rmdir', 'c', undefined] | ||
['unlink', 'c/d.js', file('c/d.js', { mode: '0o666', size: 1, mtime: 1, meta: { rev: 0 } })], | ||
['rmdir', 'c/', directory('c/')], | ||
['unlink', 'a/c.js', file('a/c.js', { mode: '0o666', size: 1, mtime: 1 })], | ||
]); | ||
}); | ||
it('should detect updates', function() { | ||
it('detects file updates', function() { | ||
var entries = [ | ||
entry({ relativePath: 'a/b.js', mode: '0o666', size: 1, mtime: 1 }), | ||
entry({ relativePath: 'c/d.js', mode: '0o666', size: 1, mtime: 2 }), | ||
entry({ relativePath: 'a/c.js', mode: '0o666', size: 10, mtime: 1 }) | ||
directory('a/'), | ||
file('a/b.js', { mode: '0o666', size: 1, mtime: 2 }), | ||
file('a/c.js', { mode: '0o666', size: 10, mtime: 1 }), | ||
directory('c/'), | ||
file('c/d.js', { mode: '0o666', size: 1, mtime: 1, meta: { rev: 1 } }), | ||
]; | ||
@@ -247,82 +376,49 @@ | ||
entries: entries | ||
})); | ||
}), userProvidedIsEqual); | ||
expect(result).to.deep.equal([ | ||
['change', 'c/d.js', entries[1]], | ||
['change', 'a/b.js', entries[1]], | ||
['change', 'a/c.js', entries[2]], | ||
['change', 'c/d.js', entries[4]], | ||
]); | ||
}); | ||
}); | ||
context('of directories', function() { | ||
it('detects linked directory additions', function() { | ||
fsTree = new FSTree({ | ||
entries: [], | ||
}); | ||
var result = fsTree.calculatePatch(new FSTree({ | ||
entries: [ | ||
directory('a', { linkDir: true }), | ||
] | ||
})); | ||
it('detects directory updates from user-supplied meta', function () { | ||
var entries = [ | ||
directory('a/', { meta: { link: true } }), | ||
file('a/b.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
file('a/c.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
directory('c/'), | ||
file('c/d.js', { mode: '0o666', size: 1, mtime: 1, meta: { rev: 0 } }) | ||
]; | ||
expect(result).to.deep.equal([ | ||
['linkdir', 'a', directory('a', { linkDir: true })] | ||
]); | ||
}); | ||
it('detects nested linked directory additions', function() { | ||
fsTree = new FSTree({ | ||
entries: [], | ||
}); | ||
var result = fsTree.calculatePatch(new FSTree({ | ||
entries: [ | ||
directory('a/b/c/d1', { linkDir: true }), | ||
file('a/b/c/d2'), | ||
] | ||
})); | ||
entries: entries | ||
}), userProvidedIsEqual); | ||
expect(result).to.deep.equal([ | ||
['mkdir', 'a', directory('a', { linkDir: false })], | ||
['mkdir', 'a/b', directory('a/b', { linkDir: false })], | ||
['mkdir', 'a/b/c', directory('a/b/c', { linkDir: false })], | ||
['linkdir', 'a/b/c/d1', directory('a/b/c/d1', { linkDir: true })], | ||
['create', 'a/b/c/d2', file('a/b/c/d2')], | ||
['change', 'a/', entries[0]] | ||
]); | ||
}); | ||
it('detects linked directory removals', function() { | ||
fsTree = new FSTree({ | ||
entries: [ | ||
directory('a', { linkDir: true }), | ||
], | ||
it('passes the rhs user-supplied entry on updates', function () { | ||
var bEntry = file('a/b.js', { | ||
mode: '0o666', size: 1, mtime: 2, meta: { link: true } | ||
}); | ||
var result = fsTree.calculatePatch(new FSTree({ | ||
entries: [] | ||
})); | ||
var entries = [ | ||
directory('a/'), | ||
bEntry, | ||
file('a/c.js', { mode: '0o666', size: 1, mtime: 1 }), | ||
directory('c/'), | ||
file('c/d.js', { mode: '0o666', size: 1, mtime: 1, meta: { rev: 0 } }), | ||
]; | ||
expect(result).to.deep.equal([ | ||
['unlinkdir', 'a', undefined ] | ||
]); | ||
}); | ||
it('detects nested linked directory removals', function() { | ||
fsTree = new FSTree({ | ||
entries: [ | ||
directory('a/b/c/d1', { linkDir: true }), | ||
file('a/b/c/d2'), | ||
], | ||
}); | ||
var result = fsTree.calculatePatch(new FSTree({ | ||
entries: [] | ||
entries: entries | ||
})); | ||
expect(result).to.deep.equal([ | ||
['unlinkdir', 'a/b/c/d1', undefined], | ||
['unlink', 'a/b/c/d2', undefined], | ||
['rmdir', 'a/b/c', undefined], | ||
['rmdir', 'a/b', undefined], | ||
['rmdir', 'a', undefined], | ||
['change', 'a/b.js', bEntry], | ||
]); | ||
}); | ||
}); | ||
@@ -368,2 +464,3 @@ }); | ||
fsTree = FSTree.fromPaths([ | ||
'bar/', | ||
'bar/baz.js', | ||
@@ -376,2 +473,3 @@ 'foo.js', | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([ | ||
'bar/', | ||
'bar/baz.js', | ||
@@ -389,6 +487,8 @@ 'foo.js' | ||
fsTree = FSTree.fromPaths([ | ||
'bar/', | ||
'bar/one.js', | ||
'bar/two.js', | ||
'foo/', | ||
'foo/one.js', | ||
'foo/two.js', | ||
'bar/one.js', | ||
'bar/two.js', | ||
]); | ||
@@ -400,8 +500,9 @@ }); | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([ | ||
'bar/', | ||
'bar/two.js' | ||
]))).to.deep.equal([ | ||
['unlink', 'foo/one.js', undefined], | ||
['unlink', 'foo/two.js', undefined], | ||
['unlink', 'bar/one.js', undefined], | ||
['rmdir', 'foo', undefined], | ||
['unlink', 'bar/one.js', file('bar/one.js')], | ||
['unlink', 'foo/two.js', file('foo/two.js')], | ||
['unlink', 'foo/one.js', file('foo/one.js')], | ||
['rmdir', 'foo/', directory('foo/')], | ||
]); | ||
@@ -414,19 +515,11 @@ }); | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([ | ||
'bar/', | ||
'bar/three.js' | ||
]))).to.deep.equal([ | ||
['unlink', 'foo/one.js', undefined], | ||
['unlink', 'foo/two.js', undefined], | ||
['unlink', 'bar/one.js', undefined], | ||
['unlink', 'bar/two.js', undefined], | ||
['rmdir', 'foo', undefined], | ||
// TODO: we could detect this NOOP [[rmdir bar] => [mkdir bar]] , but leaving it made File -> | ||
// Folder & Folder -> File transitions easiest. Maybe some future | ||
// work can explore, but the overhead today appears to be | ||
// neglibable | ||
['rmdir', 'bar', undefined], | ||
['mkdir', 'bar', directory('bar')], | ||
['create', 'bar/three.js', file('bar/three.js')] | ||
['unlink', 'bar/one.js', file('bar/one.js')], | ||
['create', 'bar/three.js', file('bar/three.js')], | ||
['unlink', 'foo/two.js', file('foo/two.js')], | ||
['unlink', 'foo/one.js', file('foo/one.js')], | ||
['rmdir', 'foo/', directory('foo/')], | ||
['unlink', 'bar/two.js', file('bar/two.js')], | ||
]); | ||
@@ -440,2 +533,4 @@ }); | ||
fsTree = FSTree.fromPaths([ | ||
'bar/', | ||
'bar/quz/', | ||
'bar/quz/baz.js', | ||
@@ -449,6 +544,6 @@ 'foo.js', | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([]))).to.deep.equal([ | ||
['unlink', 'bar/quz/baz.js', undefined], | ||
['rmdir', 'bar/quz', undefined], | ||
['rmdir', 'bar', undefined], | ||
['unlink', 'foo.js', undefined] | ||
['unlink', 'foo.js', file('foo.js')], | ||
['unlink', 'bar/quz/baz.js', file('bar/quz/baz.js')], | ||
['rmdir', 'bar/quz/', directory('bar/quz/')], | ||
['rmdir', 'bar/', directory('bar/')], | ||
]); | ||
@@ -462,4 +557,6 @@ }); | ||
fsTree = FSTree.fromPaths([ | ||
'bar/', | ||
'bar/foo.js', | ||
'bar/quz/', | ||
'bar/quz/baz.js', | ||
'bar/foo.js', | ||
]); | ||
@@ -471,5 +568,7 @@ }); | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([ | ||
'bar/', | ||
'bar/quz/', | ||
'bar/quz/baz.js' | ||
]))).to.deep.equal([ | ||
['unlink', 'bar/foo.js', undefined] | ||
['unlink', 'bar/foo.js', file('bar/foo.js')] | ||
]); | ||
@@ -483,3 +582,6 @@ }); | ||
fsTree = FSTree.fromPaths([ | ||
'subdir1/', | ||
'subdir1/subsubdir1/', | ||
'subdir1/subsubdir1/foo.png', | ||
'subdir2/', | ||
'subdir2/bar.css' | ||
@@ -492,6 +594,8 @@ ]); | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([ | ||
'subdir1/', | ||
'subdir1/subsubdir1/', | ||
'subdir1/subsubdir1/foo.png' | ||
]))).to.deep.equal([ | ||
['unlink', 'subdir2/bar.css', undefined], | ||
['rmdir', 'subdir2', undefined] | ||
['unlink', 'subdir2/bar.css', file('subdir2/bar.css')], | ||
['rmdir', 'subdir2/', directory('subdir2/')] | ||
]); | ||
@@ -505,2 +609,3 @@ }); | ||
fsTree = FSTree.fromPaths([ | ||
'subdir1/', | ||
'subdir1/foo' | ||
@@ -514,5 +619,5 @@ ]); | ||
]))).to.deep.equal([ | ||
['unlink', 'subdir1/foo', undefined], | ||
['rmdir', 'subdir1', undefined], | ||
['create', 'subdir1', file('subdir1')] | ||
['create', 'subdir1', file('subdir1')], | ||
['unlink', 'subdir1/foo', file('subdir1/foo')], | ||
['rmdir', 'subdir1/', directory('subdir1/')], | ||
]); | ||
@@ -531,6 +636,7 @@ }); | ||
expect(fsTree.calculatePatch(FSTree.fromPaths([ | ||
'subdir1/', | ||
'subdir1/foo' | ||
]))).to.deep.equal([ | ||
['unlink', 'subdir1', undefined], | ||
['mkdir', 'subdir1', directory('subdir1')], | ||
['unlink', 'subdir1', file('subdir1')], | ||
['mkdir', 'subdir1/', directory('subdir1/')], | ||
['create', 'subdir1/foo', file('subdir1/foo')] | ||
@@ -545,3 +651,5 @@ ]); | ||
'dir/', | ||
'dir2/', | ||
'dir2/subdir1/', | ||
'dir3/', | ||
'dir3/subdir1/' | ||
@@ -553,2 +661,3 @@ ]); | ||
var result = fsTree.calculatePatch(FSTree.fromPaths([ | ||
'dir2/', | ||
'dir2/subdir1/', | ||
@@ -560,13 +669,92 @@ 'dir3/', | ||
expect(result).to.deep.equal([ | ||
['rmdir', 'dir3/subdir1', undefined], | ||
['rmdir', 'dir', undefined], | ||
// This no-op (rmdir dir3; mkdir dir3) is not fundamental: a future | ||
// iteration could reasonably optimize it away | ||
['rmdir', 'dir3', undefined], | ||
['mkdir', 'dir3', directory('dir3')], | ||
['mkdir', 'dir4', directory('dir4')] | ||
['mkdir', 'dir4/', directory('dir4/')], | ||
['rmdir', 'dir3/subdir1/', directory('dir3/subdir1/')], | ||
['rmdir', 'dir/', directory('dir/')], | ||
]); | ||
}); | ||
}); | ||
context('walk-sync like tree', function () { | ||
beforeEach( function() { | ||
fsTree = new FSTree({ | ||
entries: [ | ||
entry(directory('parent/')), | ||
entry(directory('parent/subdir/')), | ||
entry(file('parent/subdir/a.js')) | ||
] | ||
}); | ||
}); | ||
it('moving a file out of a directory does not edit directory structure', function () { | ||
var newTree = new FSTree({ | ||
entries: [ | ||
entry(directory('parent/')), | ||
entry(file('parent/a.js')), | ||
entry(directory('parent/subdir/')), | ||
] | ||
}); | ||
var result = fsTree.calculatePatch(newTree); | ||
expect(result).to.deep.equal([ | ||
['create', 'parent/a.js', file('parent/a.js')], | ||
['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], | ||
]); | ||
}); | ||
it('moving a file out of a subdir and removing the subdir does not recreate parent', function () { | ||
var newTree = new FSTree({ | ||
entries: [ | ||
entry(directory('parent/')), | ||
entry(file('parent/a.js')) | ||
] | ||
}); | ||
var result = fsTree.calculatePatch(newTree); | ||
expect(result).to.deep.equal([ | ||
['create', 'parent/a.js', file('parent/a.js')], | ||
['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], | ||
['rmdir', 'parent/subdir/', directory('parent/subdir/')], | ||
]); | ||
}); | ||
it('moving a file into nest subdir does not recreate subdir and parent', function () { | ||
var newTree = new FSTree({ | ||
entries: [ | ||
entry(directory('parent/')), | ||
entry(directory('parent/subdir/')), | ||
entry(directory('parent/subdir/subdir/')), | ||
entry(file('parent/subdir/subdir/a.js')) | ||
] | ||
}); | ||
var result = fsTree.calculatePatch(newTree); | ||
expect(result).to.deep.equal([ | ||
['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], | ||
['mkdir', 'parent/subdir/subdir/', directory('parent/subdir/subdir/')], | ||
['create', 'parent/subdir/subdir/a.js', file('parent/subdir/subdir/a.js')], | ||
]); | ||
}); | ||
it('renaming a subdir does not recreate parent', function () { | ||
var newTree = new FSTree({ | ||
entries: [ | ||
entry(directory('parent/')), | ||
entry(directory('parent/subdir2/')), | ||
entry(file('parent/subdir2/a.js')) | ||
] | ||
}); | ||
var result = fsTree.calculatePatch(newTree); | ||
expect(result).to.deep.equal([ | ||
['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], | ||
['mkdir', 'parent/subdir2/', directory('parent/subdir2/')], | ||
['create', 'parent/subdir2/a.js', file('parent/subdir2/a.js')], | ||
['rmdir', 'parent/subdir/', directory('parent/subdir/')], | ||
]); | ||
}); | ||
}); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
39414
942
204
11