feat(broccoli): improve merge-trees plugin and add "overwrite" option

This commit is contained in:
Caitlin Potter 2015-06-04 17:39:53 -04:00
parent c593dfc26c
commit dc8dac7c35
2 changed files with 115 additions and 61 deletions

View File

@ -21,7 +21,7 @@ describe('MergeTrees', () => {
function read(path) { return fs.readFileSync(path, "utf-8"); } function read(path) { return fs.readFileSync(path, "utf-8"); }
it('should copy the file from the right-most inputTree', () => { it('should copy the file from the right-most inputTree with overwrite=true', () => {
let testDir: any = { let testDir: any = {
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})}, 'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})}, 'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})},
@ -29,7 +29,7 @@ describe('MergeTrees', () => {
}; };
mockfs(testDir); mockfs(testDir);
let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']); let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {}); let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {overwrite: true});
treeMerger.rebuild(treeDiffer.diffTrees()); treeMerger.rebuild(treeDiffer.diffTrees());
expect(read('dest/foo.js')).toBe('tree3/foo.js content'); expect(read('dest/foo.js')).toBe('tree3/foo.js content');
@ -44,4 +44,25 @@ describe('MergeTrees', () => {
treeMerger.rebuild(treeDiffer.diffTrees()); treeMerger.rebuild(treeDiffer.diffTrees());
expect(read('dest/foo.js')).toBe('tree2/foo.js content'); expect(read('dest/foo.js')).toBe('tree2/foo.js content');
}); });
it('should throw if duplicates are used by default', () => {
let testDir: any = {
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})},
'tree3': {'foo.js': mockfs.file({content: 'tree3/foo.js content', mtime: new Date(1000)})}
};
mockfs(testDir);
let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {});
expect(() => treeMerger.rebuild(treeDiffer.diffTrees())).toThrow();
delete testDir.tree2['foo.js'];
delete testDir.tree3['foo.js'];
mockfs(testDir);
expect(() => treeMerger.rebuild(treeDiffer.diffTrees())).not.toThrow();
testDir.tree2['foo.js'] = mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)});
mockfs(testDir);
expect(() => treeMerger.rebuild(treeDiffer.diffTrees())).toThrow();
});
}); });

View File

@ -4,81 +4,114 @@ import path = require('path');
var symlinkOrCopySync = require('symlink-or-copy').sync; var symlinkOrCopySync = require('symlink-or-copy').sync;
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin'; import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
function pathExists(filePath) { interface MergeTreesOptions {
try { overwrite?: boolean;
if (fs.statSync(filePath)) {
return true;
}
} catch (e) {
if (e.code !== "ENOENT") {
throw e;
}
}
return false;
} }
function outputFileSync(sourcePath, destPath) { function outputFileSync(sourcePath, destPath) {
let dirname = path.dirname(destPath); let dirname = path.dirname(destPath);
fse.mkdirsSync(dirname, {fs: fs}); fse.mkdirsSync(dirname, {fs: fs});
fse.removeSync(destPath);
symlinkOrCopySync(sourcePath, destPath); symlinkOrCopySync(sourcePath, destPath);
} }
export class MergeTrees implements DiffingBroccoliPlugin { export class MergeTrees implements DiffingBroccoliPlugin {
private mergedPaths: {[key: string]: number} = Object.create(null); private pathCache: {[key: string]: number[]} = Object.create(null);
public options: MergeTreesOptions;
private firstBuild: boolean = true;
constructor(public inputPaths: string[], public cachePath: string, public options) {} constructor(public inputPaths: string[], public cachePath: string,
options: MergeTreesOptions = {}) {
this.options = options || {};
}
rebuild(treeDiffs: DiffResult[]) { rebuild(treeDiffs: DiffResult[]) {
treeDiffs.forEach((treeDiff: DiffResult, index) => { let overwrite = this.options.overwrite;
let inputPath = this.inputPaths[index]; let pathsToEmit: string[] = [];
let existsLater = (relativePath) => { let pathsToRemove: string[] = [];
for (let i = treeDiffs.length - 1; i > index; --i) { let emitted: {[key: string]: boolean} = Object.create(null);
if (pathExists(path.join(this.inputPaths[i], relativePath))) { let contains = (cache, val) => {
return true; for (let i = 0, ii = cache.length; i < ii; ++i) {
} if (cache[i] === val) return true;
} }
return false; return false;
}; };
let existsSooner = (relativePath) => {
for (let i = index - 1; i >= 0; --i) { let emit = (relativePath) => {
if (pathExists(path.join(this.inputPaths[i], relativePath))) { // ASSERT(!emitted[relativePath]);
return i; pathsToEmit.push(relativePath);
} emitted[relativePath] = true;
}
return -1;
}; };
if (this.firstBuild) {
// Build initial cache
treeDiffs.reverse().forEach((treeDiff: DiffResult, index) => {
index = treeDiffs.length - 1 - index;
treeDiff.changedPaths.forEach((changedPath) => { treeDiff.changedPaths.forEach((changedPath) => {
let inputTreeIndex = this.mergedPaths[changedPath]; let cache = this.pathCache[changedPath];
if (inputTreeIndex !== index && !existsLater(changedPath)) { if (cache === undefined) {
inputTreeIndex = this.mergedPaths[changedPath] = index; this.pathCache[changedPath] = [index];
let sourcePath = path.join(inputPath, changedPath); pathsToEmit.push(changedPath);
let destPath = path.join(this.cachePath, changedPath); } else if (overwrite) {
outputFileSync(sourcePath, destPath); // ASSERT(contains(pathsToEmit, changedPath));
cache.unshift(index);
} else {
throw new Error("`overwrite` option is required for handling duplicates.");
} }
}); });
});
this.firstBuild = false;
} else {
// Update cache
treeDiffs.reverse().forEach((treeDiff: DiffResult, index) => {
index = treeDiffs.length - 1 - index;
treeDiff.removedPaths.forEach((removedPath) => { treeDiff.removedPaths.forEach((removedPath) => {
let inputTreeIndex = this.mergedPaths[removedPath]; let cache = this.pathCache[removedPath];
// ASSERT(cache !== undefined);
// if inputTreeIndex !== index, this same file was handled during // ASSERT(contains(cache, index));
// changedPaths handling if (cache[cache.length - 1] === index) {
if (inputTreeIndex !== index) return; pathsToRemove.push(path.join(this.cachePath, removedPath));
cache.pop();
let destPath = path.join(this.cachePath, removedPath); if (cache.length === 0) {
fse.removeSync(destPath); this.pathCache[removedPath] = undefined;
let newInputTreeIndex = existsSooner(removedPath); } else if (!emitted[removedPath]) {
if (cache.length === 1 && !overwrite) {
// Update cached value (to either newInputTreeIndex value or undefined) throw new Error("`overwrite` option is required for handling duplicates.");
this.mergedPaths[removedPath] = newInputTreeIndex; }
emit(removedPath);
if (newInputTreeIndex >= 0) { }
// Copy the file from the newInputTreeIndex inputPath if necessary.
let newInputPath = this.inputPaths[newInputTreeIndex];
let sourcePath = path.join(newInputPath, removedPath);
outputFileSync(sourcePath, destPath);
} }
}); });
treeDiff.changedPaths.forEach((changedPath) => {
let cache = this.pathCache[changedPath];
if (cache === undefined) {
// File was added
this.pathCache[changedPath] = [index];
emit(changedPath);
} else if (!contains(cache, index)) {
cache.push(index);
cache.sort((a, b) => a - b);
if (cache.length > 1 && !overwrite) {
throw new Error("`overwrite` option is required for handling duplicates.");
}
if (cache[cache.length - 1] === index && !emitted[changedPath]) {
emit(changedPath);
}
}
});
});
}
pathsToRemove.forEach((destPath) => fse.removeSync(destPath));
pathsToEmit.forEach((emittedPath) => {
let cache = this.pathCache[emittedPath];
let destPath = path.join(this.cachePath, emittedPath);
let sourceIndex = cache[cache.length - 1];
let sourceInputPath = this.inputPaths[sourceIndex];
let sourcePath = path.join(sourceInputPath, emittedPath);
if (cache.length > 1) {
fse.removeSync(destPath);
}
outputFileSync(sourcePath, destPath);
}); });
} }
} }