-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfileTracker.ts
138 lines (124 loc) · 4.02 KB
/
fileTracker.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { Nullable } from '@salesforce/ts-types';
import { Context } from './types';
/* eslint-disable no-await-in-loop */
/**
* This class maintains a map of tracked files. This is particularly useful
* for determining if files have changed after a command has been executed
*/
export class FileTracker {
private files = new Map<string, FileTracker.FileHistory[]>();
public constructor(private context: Context) {}
/**
* Add a file to be tracked
*/
public async track(file: string): Promise<void> {
const entry = {
annotation: 'initial state',
hash: await this.getContentHash(file),
changedFromPrevious: false,
name: file,
};
this.files.set(file, [entry]);
}
/**
* Returns tracked file's history
*/
public get(file: string): FileTracker.FileHistory[] {
return this.files.get(file) ?? [];
}
/**
* Returns latest entry for file
*/
public getLatest(file: string): Nullable<FileTracker.FileHistory> {
const history = this.files.get(file);
return history ? history[history.length - 1] : null;
}
/**
* Update the file history for given file. Annotation is required since
* it is useful for debugging/understanding a file's history
*/
public async update(file: string, annotation: string): Promise<void> {
if (!this.files.has(file)) {
await this.track(file);
return;
}
const latestHash = await this.getContentHash(file);
const entries = this.files.get(file)!;
const lastEntry = entries[entries.length - 1];
const newEntry = {
annotation,
hash: latestHash,
changedFromPrevious: lastEntry.hash !== latestHash,
name: file,
};
this.files.set(file, [...entries, newEntry]);
}
/**
* Update the history for all tracked files. Annotation is required since
* it is useful for debugging/understanding a file's history
*/
public async updateAll(annotation: string): Promise<void> {
const files = this.files.keys();
for (const file of files) {
await this.update(file, annotation);
}
}
private async getContentHash(file: string): Promise<Nullable<string>> {
const filePath = this.getFullPath(file);
try {
const filestat = await fs.promises.stat(filePath);
const isDirectory = filestat.isDirectory();
const contents = isDirectory
? (await fs.promises.readdir(filePath)).toString()
: await fs.promises.readFile(filePath);
return crypto.createHash('sha1').update(contents).digest('hex');
} catch {
return null;
}
}
private getFullPath(file: string): string {
return file.includes(this.context.projectDir) ? file : path.join(this.context.projectDir, file);
}
}
export namespace FileTracker {
export type FileHistory = {
annotation: string;
hash: Nullable<string>;
changedFromPrevious: boolean;
name: string;
};
}
/**
* Returns all files in directory that match the filter
*/
export async function traverseForFiles(dirPath: string, regexFilter = /./, allFiles: string[] = []): Promise<string[]> {
const files = await fs.promises.readdir(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
if (fs.statSync(filePath).isDirectory()) {
allFiles = await traverseForFiles(filePath, regexFilter, allFiles);
} else if (regexFilter.test(file)) {
allFiles.push(path.join(dirPath, file));
}
}
return allFiles;
}
/**
* Returns the number of files found in directories that match the filter
*/
export async function countFiles(directories: string[], regexFilter = /./): Promise<number> {
let fileCount = 0;
for (const dir of directories) {
fileCount += (await traverseForFiles(dir, regexFilter)).length;
}
return fileCount;
}