Skip to content

Commit dc8dac7

Browse files
committed
feat(broccoli): improve merge-trees plugin and add "overwrite" option
1 parent c593dfc commit dc8dac7

File tree

2 files changed

+114
-60
lines changed

2 files changed

+114
-60
lines changed

tools/broccoli/broccoli-merge-trees.spec.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ describe('MergeTrees', () => {
2121

2222
function read(path) { return fs.readFileSync(path, "utf-8"); }
2323

24-
it('should copy the file from the right-most inputTree', () => {
24+
it('should copy the file from the right-most inputTree with overwrite=true', () => {
2525
let testDir: any = {
2626
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
2727
'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})},
2828
'tree3': {'foo.js': mockfs.file({content: 'tree3/foo.js content', mtime: new Date(1000)})}
2929
};
3030
mockfs(testDir);
3131
let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
32-
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {});
32+
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {overwrite: true});
3333
treeMerger.rebuild(treeDiffer.diffTrees());
3434
expect(read('dest/foo.js')).toBe('tree3/foo.js content');
3535

@@ -44,4 +44,25 @@ describe('MergeTrees', () => {
4444
treeMerger.rebuild(treeDiffer.diffTrees());
4545
expect(read('dest/foo.js')).toBe('tree2/foo.js content');
4646
});
47+
48+
it('should throw if duplicates are used by default', () => {
49+
let testDir: any = {
50+
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
51+
'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})},
52+
'tree3': {'foo.js': mockfs.file({content: 'tree3/foo.js content', mtime: new Date(1000)})}
53+
};
54+
mockfs(testDir);
55+
let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
56+
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {});
57+
expect(() => treeMerger.rebuild(treeDiffer.diffTrees())).toThrow();
58+
59+
delete testDir.tree2['foo.js'];
60+
delete testDir.tree3['foo.js'];
61+
mockfs(testDir);
62+
expect(() => treeMerger.rebuild(treeDiffer.diffTrees())).not.toThrow();
63+
64+
testDir.tree2['foo.js'] = mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)});
65+
mockfs(testDir);
66+
expect(() => treeMerger.rebuild(treeDiffer.diffTrees())).toThrow();
67+
});
4768
});

tools/broccoli/broccoli-merge-trees.ts

Lines changed: 91 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,81 +4,114 @@ import path = require('path');
44
var symlinkOrCopySync = require('symlink-or-copy').sync;
55
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
66

7-
function pathExists(filePath) {
8-
try {
9-
if (fs.statSync(filePath)) {
10-
return true;
11-
}
12-
} catch (e) {
13-
if (e.code !== "ENOENT") {
14-
throw e;
15-
}
16-
}
17-
return false;
7+
interface MergeTreesOptions {
8+
overwrite?: boolean;
189
}
1910

2011
function outputFileSync(sourcePath, destPath) {
2112
let dirname = path.dirname(destPath);
2213
fse.mkdirsSync(dirname, {fs: fs});
23-
fse.removeSync(destPath);
2414
symlinkOrCopySync(sourcePath, destPath);
2515
}
2616

2717
export class MergeTrees implements DiffingBroccoliPlugin {
28-
private mergedPaths: {[key: string]: number} = Object.create(null);
18+
private pathCache: {[key: string]: number[]} = Object.create(null);
19+
public options: MergeTreesOptions;
20+
private firstBuild: boolean = true;
2921

30-
constructor(public inputPaths: string[], public cachePath: string, public options) {}
22+
constructor(public inputPaths: string[], public cachePath: string,
23+
options: MergeTreesOptions = {}) {
24+
this.options = options || {};
25+
}
3126

3227
rebuild(treeDiffs: DiffResult[]) {
33-
treeDiffs.forEach((treeDiff: DiffResult, index) => {
34-
let inputPath = this.inputPaths[index];
35-
let existsLater = (relativePath) => {
36-
for (let i = treeDiffs.length - 1; i > index; --i) {
37-
if (pathExists(path.join(this.inputPaths[i], relativePath))) {
38-
return true;
28+
let overwrite = this.options.overwrite;
29+
let pathsToEmit: string[] = [];
30+
let pathsToRemove: string[] = [];
31+
let emitted: {[key: string]: boolean} = Object.create(null);
32+
let contains = (cache, val) => {
33+
for (let i = 0, ii = cache.length; i < ii; ++i) {
34+
if (cache[i] === val) return true;
35+
}
36+
return false;
37+
};
38+
39+
let emit = (relativePath) => {
40+
// ASSERT(!emitted[relativePath]);
41+
pathsToEmit.push(relativePath);
42+
emitted[relativePath] = true;
43+
};
44+
45+
if (this.firstBuild) {
46+
// Build initial cache
47+
treeDiffs.reverse().forEach((treeDiff: DiffResult, index) => {
48+
index = treeDiffs.length - 1 - index;
49+
treeDiff.changedPaths.forEach((changedPath) => {
50+
let cache = this.pathCache[changedPath];
51+
if (cache === undefined) {
52+
this.pathCache[changedPath] = [index];
53+
pathsToEmit.push(changedPath);
54+
} else if (overwrite) {
55+
// ASSERT(contains(pathsToEmit, changedPath));
56+
cache.unshift(index);
57+
} else {
58+
throw new Error("`overwrite` option is required for handling duplicates.");
59+
}
60+
});
61+
});
62+
this.firstBuild = false;
63+
} else {
64+
// Update cache
65+
treeDiffs.reverse().forEach((treeDiff: DiffResult, index) => {
66+
index = treeDiffs.length - 1 - index;
67+
treeDiff.removedPaths.forEach((removedPath) => {
68+
let cache = this.pathCache[removedPath];
69+
// ASSERT(cache !== undefined);
70+
// ASSERT(contains(cache, index));
71+
if (cache[cache.length - 1] === index) {
72+
pathsToRemove.push(path.join(this.cachePath, removedPath));
73+
cache.pop();
74+
if (cache.length === 0) {
75+
this.pathCache[removedPath] = undefined;
76+
} else if (!emitted[removedPath]) {
77+
if (cache.length === 1 && !overwrite) {
78+
throw new Error("`overwrite` option is required for handling duplicates.");
79+
}
80+
emit(removedPath);
81+
}
3982
}
40-
}
41-
return false;
42-
};
43-
let existsSooner = (relativePath) => {
44-
for (let i = index - 1; i >= 0; --i) {
45-
if (pathExists(path.join(this.inputPaths[i], relativePath))) {
46-
return i;
83+
});
84+
treeDiff.changedPaths.forEach((changedPath) => {
85+
let cache = this.pathCache[changedPath];
86+
if (cache === undefined) {
87+
// File was added
88+
this.pathCache[changedPath] = [index];
89+
emit(changedPath);
90+
} else if (!contains(cache, index)) {
91+
cache.push(index);
92+
cache.sort((a, b) => a - b);
93+
if (cache.length > 1 && !overwrite) {
94+
throw new Error("`overwrite` option is required for handling duplicates.");
95+
}
96+
if (cache[cache.length - 1] === index && !emitted[changedPath]) {
97+
emit(changedPath);
98+
}
4799
}
48-
}
49-
return -1;
50-
};
51-
treeDiff.changedPaths.forEach((changedPath) => {
52-
let inputTreeIndex = this.mergedPaths[changedPath];
53-
if (inputTreeIndex !== index && !existsLater(changedPath)) {
54-
inputTreeIndex = this.mergedPaths[changedPath] = index;
55-
let sourcePath = path.join(inputPath, changedPath);
56-
let destPath = path.join(this.cachePath, changedPath);
57-
outputFileSync(sourcePath, destPath);
58-
}
100+
});
59101
});
102+
}
60103

61-
treeDiff.removedPaths.forEach((removedPath) => {
62-
let inputTreeIndex = this.mergedPaths[removedPath];
63-
64-
// if inputTreeIndex !== index, this same file was handled during
65-
// changedPaths handling
66-
if (inputTreeIndex !== index) return;
67-
68-
let destPath = path.join(this.cachePath, removedPath);
104+
pathsToRemove.forEach((destPath) => fse.removeSync(destPath));
105+
pathsToEmit.forEach((emittedPath) => {
106+
let cache = this.pathCache[emittedPath];
107+
let destPath = path.join(this.cachePath, emittedPath);
108+
let sourceIndex = cache[cache.length - 1];
109+
let sourceInputPath = this.inputPaths[sourceIndex];
110+
let sourcePath = path.join(sourceInputPath, emittedPath);
111+
if (cache.length > 1) {
69112
fse.removeSync(destPath);
70-
let newInputTreeIndex = existsSooner(removedPath);
71-
72-
// Update cached value (to either newInputTreeIndex value or undefined)
73-
this.mergedPaths[removedPath] = newInputTreeIndex;
74-
75-
if (newInputTreeIndex >= 0) {
76-
// Copy the file from the newInputTreeIndex inputPath if necessary.
77-
let newInputPath = this.inputPaths[newInputTreeIndex];
78-
let sourcePath = path.join(newInputPath, removedPath);
79-
outputFileSync(sourcePath, destPath);
80-
}
81-
});
113+
}
114+
outputFileSync(sourcePath, destPath);
82115
});
83116
}
84117
}

0 commit comments

Comments
 (0)