build(broccoli): add tree-differ for diffing broccoli trees
This commit is contained in:
		
							parent
							
								
									32c5ab956c
								
							
						
					
					
						commit
						2f83efaac8
					
				
							
								
								
									
										190
									
								
								tools/broccoli/tree-differ.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								tools/broccoli/tree-differ.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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']); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										110
									
								
								tools/broccoli/tree-differ.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								tools/broccoli/tree-differ.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user