build(broccoli): add tree-differ for diffing broccoli trees
This commit is contained in:
parent
32c5ab956c
commit
2f83efaac8
|
@ -0,0 +1,190 @@
|
|||
/// <reference path="../typings/node/node.d.ts" />
|
||||
/// <reference path="../typings/jasmine/jasmine.d.ts" />
|
||||
|
||||
let mockfs = require('mock-fs');
|
||||
import fs = require('fs');
|
||||
import TreeDiffer = require('./tree-differ');
|
||||
|
||||
|
||||
describe('TreeDiffer', () => {
|
||||
|
||||
afterEach(() => mockfs.restore());
|
||||
|
||||
|
||||
describe('diff of changed files', () => {
|
||||
|
||||
it('should list all files but no directories during the first diff', () => {
|
||||
let testDir = {
|
||||
'dir1': {
|
||||
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
|
||||
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
|
||||
'subdir-1': {
|
||||
'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
|
||||
},
|
||||
'empty-dir': {}
|
||||
}
|
||||
};
|
||||
mockfs(testDir);
|
||||
|
||||
let differ = new TreeDiffer('dir1');
|
||||
|
||||
let diffResult = differ.diffTree();
|
||||
|
||||
expect(diffResult.changedPaths)
|
||||
.toEqual(['file-1.txt', 'file-2.txt', 'subdir-1/file-1.1.txt']);
|
||||
|
||||
expect(diffResult.removedPaths).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return empty diff if nothing has changed', () => {
|
||||
let testDir = {
|
||||
'dir1': {
|
||||
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
|
||||
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
|
||||
'subdir-1': {
|
||||
'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
|
||||
},
|
||||
}
|
||||
};
|
||||
mockfs(testDir);
|
||||
|
||||
let differ = new TreeDiffer('dir1');
|
||||
|
||||
let diffResult = differ.diffTree();
|
||||
|
||||
expect(diffResult.changedPaths).not.toEqual([]);
|
||||
expect(diffResult.removedPaths).toEqual([]);
|
||||
|
||||
diffResult = differ.diffTree();
|
||||
|
||||
expect(diffResult.changedPaths).toEqual([]);
|
||||
expect(diffResult.removedPaths).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
it('should list only changed files during the subsequent diffs', () => {
|
||||
let testDir = {
|
||||
'dir1': {
|
||||
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
|
||||
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
|
||||
'subdir-1': {
|
||||
'file-1.1.txt':
|
||||
mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
|
||||
}
|
||||
}
|
||||
};
|
||||
mockfs(testDir);
|
||||
|
||||
let differ = new TreeDiffer('dir1');
|
||||
|
||||
let diffResult = differ.diffTree();
|
||||
|
||||
expect(diffResult.changedPaths)
|
||||
.toEqual(['file-1.txt', 'file-2.txt', 'subdir-1/file-1.1.txt']);
|
||||
|
||||
// change two files
|
||||
testDir['dir1']['file-1.txt'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
|
||||
testDir['dir1']['subdir-1']['file-1.1.txt'] =
|
||||
mockfs.file({content: 'file-1.1.txt content', mtime: new Date(9999)});
|
||||
mockfs(testDir);
|
||||
|
||||
diffResult = differ.diffTree();
|
||||
|
||||
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'subdir-1/file-1.1.txt']);
|
||||
|
||||
expect(diffResult.removedPaths).toEqual([]);
|
||||
|
||||
// change one file
|
||||
testDir['dir1']['file-1.txt'] = mockfs.file({content: 'super new', mtime: new Date(1000)});
|
||||
mockfs(testDir);
|
||||
|
||||
diffResult = differ.diffTree();
|
||||
expect(diffResult.changedPaths).toEqual(['file-1.txt']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('diff of new files', () => {
|
||||
|
||||
it('should detect file additions and report them as changed files', () => {
|
||||
let testDir = {
|
||||
'dir1':
|
||||
{'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})}
|
||||
};
|
||||
mockfs(testDir);
|
||||
|
||||
let differ = new TreeDiffer('dir1');
|
||||
differ.diffTree();
|
||||
|
||||
testDir['dir1']['file-2.txt'] = 'new file';
|
||||
mockfs(testDir);
|
||||
|
||||
let diffResult = differ.diffTree();
|
||||
expect(diffResult.changedPaths).toEqual(['file-2.txt']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should detect file additions mixed with file changes', () => {
|
||||
let testDir = {
|
||||
'dir1': {'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})}
|
||||
};
|
||||
mockfs(testDir);
|
||||
|
||||
let differ = new TreeDiffer('dir1');
|
||||
differ.diffTree();
|
||||
|
||||
testDir['dir1']['file-1.txt'] = 'new content';
|
||||
testDir['dir1']['file-2.txt'] = 'new file';
|
||||
mockfs(testDir);
|
||||
|
||||
let diffResult = differ.diffTree();
|
||||
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-2.txt']);
|
||||
});
|
||||
|
||||
|
||||
describe('diff of removed files', () => {
|
||||
|
||||
it('should detect file removals and report them as removed files', () => {
|
||||
let testDir = {
|
||||
'dir1':
|
||||
{'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})}
|
||||
};
|
||||
mockfs(testDir);
|
||||
|
||||
let differ = new TreeDiffer('dir1');
|
||||
differ.diffTree();
|
||||
|
||||
delete testDir['dir1']['file-1.txt'];
|
||||
mockfs(testDir);
|
||||
|
||||
let diffResult = differ.diffTree();
|
||||
expect(diffResult.changedPaths).toEqual([]);
|
||||
expect(diffResult.removedPaths).toEqual(['file-1.txt']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should detect file removals mixed with file changes and additions', () => {
|
||||
let testDir = {
|
||||
'dir1': {
|
||||
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
|
||||
'file-2.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})
|
||||
}
|
||||
};
|
||||
|
||||
mockfs(testDir);
|
||||
|
||||
let differ = new TreeDiffer('dir1');
|
||||
differ.diffTree();
|
||||
|
||||
testDir['dir1']['file-1.txt'] = 'changed content';
|
||||
delete testDir['dir1']['file-2.txt'];
|
||||
testDir['dir1']['file-3.txt'] = 'new content';
|
||||
mockfs(testDir);
|
||||
|
||||
let diffResult = differ.diffTree();
|
||||
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-3.txt']);
|
||||
expect(diffResult.removedPaths).toEqual(['file-2.txt']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/// <reference path="../typings/node/node.d.ts" />
|
||||
|
||||
import fs = require('fs');
|
||||
import path = require('path');
|
||||
|
||||
|
||||
export = TreeDiffer;
|
||||
|
||||
class TreeDiffer {
|
||||
private fingerprints: {[key: string]: string} = Object.create(null);
|
||||
private nextFingerprints: {[key: string]: string} = Object.create(null);
|
||||
private rootDirName: string;
|
||||
|
||||
constructor(private rootPath: string) { this.rootDirName = path.basename(rootPath); }
|
||||
|
||||
|
||||
public diffTree(): DiffResult {
|
||||
let result = new DiffResult(this.rootDirName);
|
||||
this.dirtyCheckPath(this.rootPath, result);
|
||||
this.detectDeletionsAndUpdateFingerprints(result);
|
||||
result.endTime = Date.now();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private dirtyCheckPath(rootDir: string, result: DiffResult) {
|
||||
fs.readdirSync(rootDir).forEach((segment) => {
|
||||
let absolutePath = path.join(rootDir, segment);
|
||||
let pathStat = fs.statSync(absolutePath);
|
||||
|
||||
if (pathStat.isDirectory()) {
|
||||
result.directoriesChecked++;
|
||||
this.dirtyCheckPath(absolutePath, result);
|
||||
} else {
|
||||
result.filesChecked++;
|
||||
if (this.isFileDirty(absolutePath, pathStat)) {
|
||||
result.changedPaths.push(path.relative(this.rootPath, absolutePath));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private isFileDirty(path: string, stat: fs.Stats): boolean {
|
||||
let oldFingerprint = this.fingerprints[path];
|
||||
let newFingerprint = `${stat.mtime.getTime()} # ${stat.size}`;
|
||||
|
||||
this.nextFingerprints[path] = newFingerprint;
|
||||
|
||||
if (oldFingerprint) {
|
||||
this.fingerprints[path] = null;
|
||||
|
||||
if (oldFingerprint === newFingerprint) {
|
||||
// nothing changed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private detectDeletionsAndUpdateFingerprints(result: DiffResult) {
|
||||
for (let absolutePath in this.fingerprints) {
|
||||
if (this.fingerprints[absolutePath] !== null) {
|
||||
let relativePath = path.relative(this.rootPath, absolutePath);
|
||||
result.removedPaths.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
this.fingerprints = this.nextFingerprints;
|
||||
this.nextFingerprints = Object.create(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DiffResult {
|
||||
public filesChecked: number = 0;
|
||||
public directoriesChecked: number = 0;
|
||||
public changedPaths: string[] = [];
|
||||
public removedPaths: string[] = [];
|
||||
public startTime: number = Date.now();
|
||||
public endTime: number = null;
|
||||
|
||||
constructor(public name: string) {}
|
||||
|
||||
toString() {
|
||||
return `${pad(this.name, 40)}, ` +
|
||||
`duration: ${pad(this.endTime - this.startTime, 5)}ms, ` +
|
||||
`${pad(this.changedPaths.length + this.removedPaths.length, 5)} changes detected ` +
|
||||
`(files: ${pad(this.filesChecked, 5)}, directories: ${pad(this.directoriesChecked, 4)})`;
|
||||
}
|
||||
|
||||
log(verbose) {
|
||||
let prefixedPaths =
|
||||
this.changedPaths.map((p) => `* ${p}`).concat(this.removedPaths.map((p) => `- ${p}`));
|
||||
console.log(`Tree diff: ${this}` +
|
||||
((verbose && prefixedPaths.length) ? ` [\n ${prefixedPaths.join('\n ')}\n]` : ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function pad(value, length) {
|
||||
value = '' + value;
|
||||
let whitespaceLength = (value.length < length) ? length - value.length : 0;
|
||||
whitespaceLength = whitespaceLength + 1;
|
||||
return new Array(whitespaceLength).join(' ') + value;
|
||||
}
|
Loading…
Reference in New Issue