Skip to content

Commit

Permalink
[package-deps-hash] Expose hashFilesAsync API (#4934)
Browse files Browse the repository at this point in the history
* [package-deps-hash] Expose `hashFilesAsync` API

* Update comments

* rush change

* Expose from index

---------

Co-authored-by: David Michon <[email protected]>
  • Loading branch information
dmichon-msft and dmichon-msft authored Sep 20, 2024
1 parent 23ed83a commit d7b6ad0
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/package-deps-hash",
"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.",
"type": "minor"
}
],
"packageName": "@rushstack/package-deps-hash"
}
3 changes: 3 additions & 0 deletions common/reviews/api/package-deps-hash.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export function getRepoRoot(currentWorkingDirectory: string, gitPath?: string):
// @beta
export function getRepoStateAsync(rootDirectory: string, additionalRelativePathsToHash?: string[], gitPath?: string): Promise<Map<string, string>>;

// @beta
export function hashFilesAsync(rootDirectory: string, filesToHash: Iterable<string> | AsyncIterable<string>, gitPath?: string): Promise<Iterable<[string, string]>>;

// @beta
export interface IFileDiffStatus {
// (undocumented)
Expand Down
81 changes: 62 additions & 19 deletions libraries/package-deps-hash/src/getRepoState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,61 @@ async function spawnGitAsync(
return stdout;
}

function isIterable<T>(value: Iterable<T> | AsyncIterable<T>): value is Iterable<T> {
return Symbol.iterator in value;
}

/**
* Uses `git hash-object` to hash the provided files. Unlike `getGitHashForFiles`, this API is asynchronous, and also allows for
* the input file paths to be specified as an async iterable.
*
* @param rootDirectory - The root directory to which paths are specified relative. Must be the root of the Git repository.
* @param filesToHash - The file paths to hash using `git hash-object`
* @param gitPath - The path to the Git executable
* @returns An iterable of [filePath, hash] pairs
*
* @remarks
* The input file paths must be specified relative to the Git repository root, or else be absolute paths.
* @beta
*/
export async function hashFilesAsync(
rootDirectory: string,
filesToHash: Iterable<string> | AsyncIterable<string>,
gitPath?: string
): Promise<Iterable<[string, string]>> {
const hashPaths: string[] = [];

const input: Readable = Readable.from(
isIterable(filesToHash)
? (function* (): IterableIterator<string> {
for (const file of filesToHash) {
hashPaths.push(file);
yield `${file}\n`;
}
})()
: (async function* (): AsyncIterableIterator<string> {
for await (const file of filesToHash) {
hashPaths.push(file);
yield `${file}\n`;
}
})(),
{
encoding: 'utf-8',
objectMode: false,
autoDestroy: true
}
);

const hashObjectResult: string = await spawnGitAsync(
gitPath,
STANDARD_GIT_OPTIONS.concat(['hash-object', '--stdin-paths']),
rootDirectory,
input
);

return parseGitHashObject(hashObjectResult, hashPaths);
}

/**
* Gets the object hashes for all files in the Git repo, combining the current commit with working tree state.
* Uses async operations and runs all primary Git calls in parallel.
Expand Down Expand Up @@ -346,46 +401,34 @@ export async function getRepoStateAsync(
rootDirectory
).then(parseGitStatus);

const hashPaths: string[] = [];
async function* getFilesToHash(): AsyncIterableIterator<string> {
if (additionalRelativePathsToHash) {
for (const file of additionalRelativePathsToHash) {
hashPaths.push(file);
yield `${file}\n`;
yield file;
}
}

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

for (const [filePath, exists] of locallyModified) {
if (exists) {
hashPaths.push(filePath);
yield `${filePath}\n`;
yield filePath;
} else {
files.delete(filePath);
}
}
}

const hashObjectPromise: Promise<string> = spawnGitAsync(
gitPath,
STANDARD_GIT_OPTIONS.concat(['hash-object', '--stdin-paths']),
const hashObjectPromise: Promise<Iterable<[string, string]>> = hashFilesAsync(
rootDirectory,
Readable.from(getFilesToHash(), {
encoding: 'utf-8',
objectMode: false,
autoDestroy: true
})
getFilesToHash(),
gitPath
);

const [{ files, submodules }, hashObject] = await Promise.all([
statePromise,
hashObjectPromise,
locallyModifiedPromise
]);
const [{ files, submodules }] = await Promise.all([statePromise, locallyModifiedPromise]);

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

Expand Down
3 changes: 2 additions & 1 deletion libraries/package-deps-hash/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export {
getRepoChanges,
getRepoRoot,
getRepoStateAsync,
ensureGitMinimumVersion
ensureGitMinimumVersion,
hashFilesAsync
} from './getRepoState';

0 comments on commit d7b6ad0

Please sign in to comment.