Skip to content

Commit aad5795

Browse files
committed
fix(tree-differ): treat symlinks to deleted paths as removals
Previously, tree-differ would not correctly handle symlinks to deleted files, resulting in an ENOENT errno being tossed by libuv. This change fixes this to ensure that symlinks are safely handled, performantly. Closes angular#1961
1 parent 83b97c4 commit aad5795

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

tools/broccoli/tree-differ.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,79 @@ describe('TreeDiffer', () => {
104104
});
105105

106106

107+
it('should handle changes via symbolic links', () => {
108+
let testDir = {
109+
'orig_path': {
110+
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
111+
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
112+
'subdir-1': {
113+
'file-1.1.txt':
114+
mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
115+
}
116+
},
117+
'symlinks': {
118+
'file-1.txt': mockfs.symlink({path: '../orig_path/file-1.txt'}),
119+
'file-2.txt': mockfs.symlink({path: '../orig_path/file-2.txt'}),
120+
'subdir-1':
121+
{'file-1.1.txt': mockfs.symlink({path: '../../orig_path/subdir-1/file-1.1.txt'})}
122+
}
123+
};
124+
mockfs(testDir);
125+
126+
let differ = new TreeDiffer('symlinks');
127+
128+
let diffResult = differ.diffTree();
129+
130+
expect(diffResult.changedPaths)
131+
.toEqual(['file-1.txt', 'file-2.txt', 'subdir-1/file-1.1.txt']);
132+
133+
// change two files
134+
testDir['orig_path']['file-1.txt'] =
135+
mockfs.file({content: 'new content', mtime: new Date(1000)});
136+
testDir['orig_path']['subdir-1']['file-1.1.txt'] =
137+
mockfs.file({content: 'file-1.1.txt content', mtime: new Date(9999)});
138+
mockfs(testDir);
139+
140+
diffResult = differ.diffTree();
141+
142+
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'subdir-1/file-1.1.txt']);
143+
144+
expect(diffResult.removedPaths).toEqual([]);
145+
146+
// change one file
147+
testDir['orig_path']['file-1.txt'] =
148+
mockfs.file({content: 'super new', mtime: new Date(1000)});
149+
mockfs(testDir);
150+
151+
diffResult = differ.diffTree();
152+
expect(diffResult.changedPaths).toEqual(['file-1.txt']);
153+
154+
// remove a link
155+
delete testDir['orig_path']['file-1.txt'];
156+
mockfs(testDir);
157+
158+
diffResult = differ.diffTree();
159+
expect(diffResult.changedPaths).toEqual([]);
160+
expect(diffResult.removedPaths).toEqual(['file-1.txt']);
161+
162+
// don't report it as a removal twice
163+
mockfs(testDir);
164+
165+
diffResult = differ.diffTree();
166+
expect(diffResult.changedPaths).toEqual([]);
167+
expect(diffResult.removedPaths).toEqual([]);
168+
169+
// re-add it.
170+
testDir['orig_path']['file-1.txt'] =
171+
mockfs.file({content: 'super new', mtime: new Date(1000)});
172+
mockfs(testDir);
173+
174+
diffResult = differ.diffTree();
175+
expect(diffResult.changedPaths).toEqual(['file-1.txt']);
176+
expect(diffResult.removedPaths).toEqual([]);
177+
});
178+
179+
107180
it('should ignore files with extensions not listed in includeExtensions', () => {
108181
let testDir = {
109182
'dir1': {

tools/broccoli/tree-differ.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import fs = require('fs');
44
import path = require('path');
55

66

7+
function tryStatSync(path) {
8+
try {
9+
return fs.statSync(path);
10+
} catch (e) {
11+
if (e.code === "ENOENT") return null;
12+
throw e;
13+
}
14+
}
15+
16+
717
export class TreeDiffer {
818
private fingerprints: {[key: string]: string} = Object.create(null);
919
private nextFingerprints: {[key: string]: string} = Object.create(null);
@@ -41,7 +51,11 @@ export class TreeDiffer {
4151
private dirtyCheckPath(rootDir: string, result: DirtyCheckingDiffResult) {
4252
fs.readdirSync(rootDir).forEach((segment) => {
4353
let absolutePath = path.join(rootDir, segment);
44-
let pathStat = fs.statSync(absolutePath);
54+
let pathStat = fs.lstatSync(absolutePath);
55+
if (pathStat.isSymbolicLink()) {
56+
pathStat = tryStatSync(absolutePath);
57+
if (pathStat === null) return;
58+
}
4559

4660
if (pathStat.isDirectory()) {
4761
result.directoriesChecked++;

0 commit comments

Comments
 (0)