Skip to content

Commit d7b6ad0

Browse files
authored
[package-deps-hash] Expose hashFilesAsync API (#4934)
* [package-deps-hash] Expose `hashFilesAsync` API * Update comments * rush change * Expose from index --------- Co-authored-by: David Michon <[email protected]>
1 parent 23ed83a commit d7b6ad0

File tree

4 files changed

+77
-20
lines changed

4 files changed

+77
-20
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/package-deps-hash",
5+
"comment": "Expose `hashFilesAsync` API. This serves a similar role as `getGitHashForFiles` but is asynchronous and allows for the file names to be provided as an async iterable.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/package-deps-hash"
10+
}

common/reviews/api/package-deps-hash.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export function getRepoRoot(currentWorkingDirectory: string, gitPath?: string):
2222
// @beta
2323
export function getRepoStateAsync(rootDirectory: string, additionalRelativePathsToHash?: string[], gitPath?: string): Promise<Map<string, string>>;
2424

25+
// @beta
26+
export function hashFilesAsync(rootDirectory: string, filesToHash: Iterable<string> | AsyncIterable<string>, gitPath?: string): Promise<Iterable<[string, string]>>;
27+
2528
// @beta
2629
export interface IFileDiffStatus {
2730
// (undocumented)

libraries/package-deps-hash/src/getRepoState.ts

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,61 @@ async function spawnGitAsync(
298298
return stdout;
299299
}
300300

301+
function isIterable<T>(value: Iterable<T> | AsyncIterable<T>): value is Iterable<T> {
302+
return Symbol.iterator in value;
303+
}
304+
305+
/**
306+
* Uses `git hash-object` to hash the provided files. Unlike `getGitHashForFiles`, this API is asynchronous, and also allows for
307+
* the input file paths to be specified as an async iterable.
308+
*
309+
* @param rootDirectory - The root directory to which paths are specified relative. Must be the root of the Git repository.
310+
* @param filesToHash - The file paths to hash using `git hash-object`
311+
* @param gitPath - The path to the Git executable
312+
* @returns An iterable of [filePath, hash] pairs
313+
*
314+
* @remarks
315+
* The input file paths must be specified relative to the Git repository root, or else be absolute paths.
316+
* @beta
317+
*/
318+
export async function hashFilesAsync(
319+
rootDirectory: string,
320+
filesToHash: Iterable<string> | AsyncIterable<string>,
321+
gitPath?: string
322+
): Promise<Iterable<[string, string]>> {
323+
const hashPaths: string[] = [];
324+
325+
const input: Readable = Readable.from(
326+
isIterable(filesToHash)
327+
? (function* (): IterableIterator<string> {
328+
for (const file of filesToHash) {
329+
hashPaths.push(file);
330+
yield `${file}\n`;
331+
}
332+
})()
333+
: (async function* (): AsyncIterableIterator<string> {
334+
for await (const file of filesToHash) {
335+
hashPaths.push(file);
336+
yield `${file}\n`;
337+
}
338+
})(),
339+
{
340+
encoding: 'utf-8',
341+
objectMode: false,
342+
autoDestroy: true
343+
}
344+
);
345+
346+
const hashObjectResult: string = await spawnGitAsync(
347+
gitPath,
348+
STANDARD_GIT_OPTIONS.concat(['hash-object', '--stdin-paths']),
349+
rootDirectory,
350+
input
351+
);
352+
353+
return parseGitHashObject(hashObjectResult, hashPaths);
354+
}
355+
301356
/**
302357
* Gets the object hashes for all files in the Git repo, combining the current commit with working tree state.
303358
* Uses async operations and runs all primary Git calls in parallel.
@@ -346,46 +401,34 @@ export async function getRepoStateAsync(
346401
rootDirectory
347402
).then(parseGitStatus);
348403

349-
const hashPaths: string[] = [];
350404
async function* getFilesToHash(): AsyncIterableIterator<string> {
351405
if (additionalRelativePathsToHash) {
352406
for (const file of additionalRelativePathsToHash) {
353-
hashPaths.push(file);
354-
yield `${file}\n`;
407+
yield file;
355408
}
356409
}
357410

358411
const [{ files }, locallyModified] = await Promise.all([statePromise, locallyModifiedPromise]);
359412

360413
for (const [filePath, exists] of locallyModified) {
361414
if (exists) {
362-
hashPaths.push(filePath);
363-
yield `${filePath}\n`;
415+
yield filePath;
364416
} else {
365417
files.delete(filePath);
366418
}
367419
}
368420
}
369421

370-
const hashObjectPromise: Promise<string> = spawnGitAsync(
371-
gitPath,
372-
STANDARD_GIT_OPTIONS.concat(['hash-object', '--stdin-paths']),
422+
const hashObjectPromise: Promise<Iterable<[string, string]>> = hashFilesAsync(
373423
rootDirectory,
374-
Readable.from(getFilesToHash(), {
375-
encoding: 'utf-8',
376-
objectMode: false,
377-
autoDestroy: true
378-
})
424+
getFilesToHash(),
425+
gitPath
379426
);
380427

381-
const [{ files, submodules }, hashObject] = await Promise.all([
382-
statePromise,
383-
hashObjectPromise,
384-
locallyModifiedPromise
385-
]);
428+
const [{ files, submodules }] = await Promise.all([statePromise, locallyModifiedPromise]);
386429

387430
// The result of "git hash-object" will be a list of file hashes delimited by newlines
388-
for (const [filePath, hash] of parseGitHashObject(hashObject, hashPaths)) {
431+
for (const [filePath, hash] of await hashObjectPromise) {
389432
files.set(filePath, hash);
390433
}
391434

libraries/package-deps-hash/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ export {
1919
getRepoChanges,
2020
getRepoRoot,
2121
getRepoStateAsync,
22-
ensureGitMinimumVersion
22+
ensureGitMinimumVersion,
23+
hashFilesAsync
2324
} from './getRepoState';

0 commit comments

Comments
 (0)