2015-04-24 13:00:38 -04:00
|
|
|
/// <reference path="../typings/node/node.d.ts" />
|
|
|
|
|
|
|
|
import fs = require('fs');
|
|
|
|
import path = require('path');
|
|
|
|
|
|
|
|
|
2015-05-18 13:39:28 -04:00
|
|
|
function tryStatSync(path) {
|
|
|
|
try {
|
|
|
|
return fs.statSync(path);
|
|
|
|
} catch (e) {
|
|
|
|
if (e.code === "ENOENT") return null;
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-05-04 11:19:25 -04:00
|
|
|
export class TreeDiffer {
|
2015-04-24 13:00:38 -04:00
|
|
|
private fingerprints: {[key: string]: string} = Object.create(null);
|
|
|
|
private nextFingerprints: {[key: string]: string} = Object.create(null);
|
|
|
|
private rootDirName: string;
|
2015-05-06 19:24:10 -04:00
|
|
|
private include: RegExp = null;
|
|
|
|
private exclude: RegExp = null;
|
2015-04-24 13:00:38 -04:00
|
|
|
|
2015-05-06 19:24:10 -04:00
|
|
|
constructor(private rootPath: string, includeExtensions?: string[],
|
|
|
|
excludeExtensions?: string[]) {
|
|
|
|
this.rootDirName = path.basename(rootPath);
|
|
|
|
|
|
|
|
let buildRegexp = (arr) => new RegExp(`(${arr.reduce(combine, "")})$`, "i");
|
|
|
|
|
|
|
|
this.include = (includeExtensions || []).length ? buildRegexp(includeExtensions) : null;
|
|
|
|
this.exclude = (excludeExtensions || []).length ? buildRegexp(excludeExtensions) : null;
|
|
|
|
|
|
|
|
function combine(prev, curr) {
|
|
|
|
if (curr.charAt(0) !== ".") throw new TypeError("Extension must begin with '.'");
|
2015-05-07 18:15:15 -04:00
|
|
|
let kSpecialRegexpChars = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
|
|
|
|
curr = '(' + curr.replace(kSpecialRegexpChars, '\\$&') + ')';
|
2015-05-06 19:24:10 -04:00
|
|
|
return prev ? (prev + '|' + curr) : curr;
|
|
|
|
}
|
|
|
|
}
|
2015-04-24 13:00:38 -04:00
|
|
|
|
|
|
|
|
|
|
|
public diffTree(): DiffResult {
|
2015-05-04 11:19:25 -04:00
|
|
|
let result = new DirtyCheckingDiffResult(this.rootDirName);
|
2015-04-24 13:00:38 -04:00
|
|
|
this.dirtyCheckPath(this.rootPath, result);
|
|
|
|
this.detectDeletionsAndUpdateFingerprints(result);
|
|
|
|
result.endTime = Date.now();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-05-04 11:19:25 -04:00
|
|
|
private dirtyCheckPath(rootDir: string, result: DirtyCheckingDiffResult) {
|
2015-04-24 13:00:38 -04:00
|
|
|
fs.readdirSync(rootDir).forEach((segment) => {
|
|
|
|
let absolutePath = path.join(rootDir, segment);
|
2015-05-18 13:39:28 -04:00
|
|
|
let pathStat = fs.lstatSync(absolutePath);
|
|
|
|
if (pathStat.isSymbolicLink()) {
|
|
|
|
pathStat = tryStatSync(absolutePath);
|
|
|
|
if (pathStat === null) return;
|
|
|
|
}
|
2015-04-24 13:00:38 -04:00
|
|
|
|
|
|
|
if (pathStat.isDirectory()) {
|
|
|
|
result.directoriesChecked++;
|
|
|
|
this.dirtyCheckPath(absolutePath, result);
|
|
|
|
} else {
|
2015-05-06 19:24:10 -04:00
|
|
|
if (!(this.include && !absolutePath.match(this.include)) &&
|
|
|
|
!(this.exclude && absolutePath.match(this.exclude))) {
|
|
|
|
result.filesChecked++;
|
|
|
|
if (this.isFileDirty(absolutePath, pathStat)) {
|
|
|
|
result.changedPaths.push(path.relative(this.rootPath, absolutePath));
|
|
|
|
}
|
2015-04-24 13:00:38 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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) {
|
2015-05-06 19:24:10 -04:00
|
|
|
if (!(this.include && !absolutePath.match(this.include)) &&
|
|
|
|
!(this.exclude && absolutePath.match(this.exclude))) {
|
|
|
|
if (this.fingerprints[absolutePath] !== null) {
|
|
|
|
let relativePath = path.relative(this.rootPath, absolutePath);
|
|
|
|
result.removedPaths.push(relativePath);
|
|
|
|
}
|
2015-04-24 13:00:38 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.fingerprints = this.nextFingerprints;
|
|
|
|
this.nextFingerprints = Object.create(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-05-04 11:19:25 -04:00
|
|
|
export interface DiffResult {
|
|
|
|
changedPaths: string[];
|
|
|
|
removedPaths: string[];
|
|
|
|
log(verbose: boolean): void;
|
|
|
|
toString(): string;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class DirtyCheckingDiffResult {
|
2015-04-24 13:00:38 -04:00
|
|
|
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() {
|
2015-05-06 19:24:10 -04:00
|
|
|
return `${pad(this.name, 40)}, duration: ${pad(this.endTime - this.startTime, 5)}ms, ` +
|
2015-04-24 13:00:38 -04:00
|
|
|
`${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}`));
|
2015-05-06 19:24:10 -04:00
|
|
|
console.log(`Tree diff: ${this}` + ((verbose && prefixedPaths.length) ?
|
|
|
|
` [\n ${prefixedPaths.join('\n ')}\n]` :
|
|
|
|
''));
|
2015-04-24 13:00:38 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pad(value, length) {
|
|
|
|
value = '' + value;
|
|
|
|
let whitespaceLength = (value.length < length) ? length - value.length : 0;
|
|
|
|
whitespaceLength = whitespaceLength + 1;
|
|
|
|
return new Array(whitespaceLength).join(' ') + value;
|
|
|
|
}
|