Socket
Socket
Sign inDemoInstall

fs-tree-diff

Package Overview
Dependencies
Maintainers
3
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fs-tree-diff - npm Package Compare versions

Comparing version 0.4.4 to 0.5.0

CHANGELOG.md

9

lib/entry.js

@@ -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);
};
'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];
}
'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",

@@ -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/')],
]);
});
});
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc