diff --git a/common/changes/@microsoft/rush/snapshot-local_2025-05-07-18-39.json b/common/changes/@microsoft/rush/snapshot-local_2025-05-07-18-39.json new file mode 100644 index 00000000000..04deb0f5213 --- /dev/null +++ b/common/changes/@microsoft/rush/snapshot-local_2025-05-07-18-39.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add `hasUncommittedChanges` to `IInputSnapshot` for use by plugins.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/package-deps-hash/snapshot-local_2025-05-07-18-39.json b/common/changes/@rushstack/package-deps-hash/snapshot-local_2025-05-07-18-39.json new file mode 100644 index 00000000000..702c3bc61d0 --- /dev/null +++ b/common/changes/@rushstack/package-deps-hash/snapshot-local_2025-05-07-18-39.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/package-deps-hash", + "comment": "Add `getDetailedRepoState` API to expose `hasSubmodules` and `hasUncommittedChanges` in addition to the results returned by `getRepoState`.", + "type": "minor" + } + ], + "packageName": "@rushstack/package-deps-hash" +} \ No newline at end of file diff --git a/common/reviews/api/package-deps-hash.api.md b/common/reviews/api/package-deps-hash.api.md index e6fd75084d2..6a18a450903 100644 --- a/common/reviews/api/package-deps-hash.api.md +++ b/common/reviews/api/package-deps-hash.api.md @@ -7,6 +7,9 @@ // @public export function ensureGitMinimumVersion(gitPath?: string): void; +// @beta +export function getDetailedRepoStateAsync(rootDirectory: string, additionalRelativePathsToHash?: string[], gitPath?: string, filterPath?: string[]): Promise; + // @public export function getGitHashForFiles(filesToHash: string[], packagePath: string, gitPath?: string): Map; @@ -25,6 +28,13 @@ export function getRepoStateAsync(rootDirectory: string, additionalRelativePaths // @beta export function hashFilesAsync(rootDirectory: string, filesToHash: Iterable | AsyncIterable, gitPath?: string): Promise>; +// @beta +export interface IDetailedRepoState { + files: Map; + hasSubmodules: boolean; + hasUncommittedChanges: boolean; +} + // @beta export interface IFileDiffStatus { // (undocumented) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index bb74a60c40e..76e081722d1 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -535,6 +535,7 @@ export interface IInputsSnapshot { getOperationOwnStateHash(project: IRushConfigurationProjectForSnapshot, operationName?: string): string; getTrackedFileHashesForOperation(project: IRushConfigurationProjectForSnapshot, operationName?: string): ReadonlyMap; readonly hashes: ReadonlyMap; + readonly hasUncommittedChanges: boolean; readonly rootDirectory: string; } diff --git a/libraries/package-deps-hash/src/getRepoState.ts b/libraries/package-deps-hash/src/getRepoState.ts index 4b9bdcb57cc..e932833e491 100644 --- a/libraries/package-deps-hash/src/getRepoState.ts +++ b/libraries/package-deps-hash/src/getRepoState.ts @@ -367,6 +367,49 @@ export async function getRepoStateAsync( gitPath?: string, filterPath?: string[] ): Promise> { + const { files } = await getDetailedRepoStateAsync( + rootDirectory, + additionalRelativePathsToHash, + gitPath, + filterPath + ); + + return files; +} + +/** + * Information about the detailed state of the Git repository. + * @beta + */ +export interface IDetailedRepoState { + /** + * The Git file hashes for all files in the repository, including uncommitted changes. + */ + files: Map; + /** + * A boolean indicating whether the repository has submodules. + */ + hasSubmodules: boolean; + /** + * A boolean indicating whether the repository has uncommitted changes. + */ + hasUncommittedChanges: boolean; +} + +/** + * 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. + * @param rootDirectory - The root directory of the Git repository + * @param additionalRelativePathsToHash - Root-relative file paths to have Git hash and include in the results + * @param gitPath - The path to the Git executable + * @beta + */ +export async function getDetailedRepoStateAsync( + rootDirectory: string, + additionalRelativePathsToHash?: string[], + gitPath?: string, + filterPath?: string[] +): Promise { const statePromise: Promise = spawnGitAsync( gitPath, STANDARD_GIT_OPTIONS.concat([ @@ -428,7 +471,10 @@ export async function getRepoStateAsync( gitPath ); - const [{ files, submodules }] = await Promise.all([statePromise, locallyModifiedPromise]); + const [{ files, submodules }, locallyModifiedFiles] = 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 await hashObjectPromise) { @@ -453,7 +499,11 @@ export async function getRepoStateAsync( } } - return files; + return { + hasSubmodules, + hasUncommittedChanges: locallyModifiedFiles.size > 0, + files + }; } /** diff --git a/libraries/package-deps-hash/src/index.ts b/libraries/package-deps-hash/src/index.ts index c6668002dbe..8558d210a4d 100644 --- a/libraries/package-deps-hash/src/index.ts +++ b/libraries/package-deps-hash/src/index.ts @@ -16,6 +16,8 @@ export { getPackageDeps, getGitHashForFiles } from './getPackageDeps'; export { type IFileDiffStatus, + type IDetailedRepoState, + getDetailedRepoStateAsync, getRepoChanges, getRepoRoot, getRepoStateAsync, diff --git a/libraries/package-deps-hash/src/test/__snapshots__/getRepoDeps.test.ts.snap b/libraries/package-deps-hash/src/test/__snapshots__/getRepoDeps.test.ts.snap index add7d8be46f..9fd6eb281ea 100644 --- a/libraries/package-deps-hash/src/test/__snapshots__/getRepoDeps.test.ts.snap +++ b/libraries/package-deps-hash/src/test/__snapshots__/getRepoDeps.test.ts.snap @@ -1,85 +1,113 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getRepoStateAsync can handle adding one file 1`] = ` +exports[`getDetailedRepoStateAsync can handle adding one file 1`] = ` Object { - "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", - "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/a.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", - "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", - "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", - "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "files": Object { + "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/a.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", + "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", + "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", + "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + }, + "hasSubmodules": false, + "hasUncommittedChanges": true, } `; -exports[`getRepoStateAsync can handle adding two files 1`] = ` +exports[`getDetailedRepoStateAsync can handle adding two files 1`] = ` Object { - "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", - "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/a.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", - "testProject/b.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", - "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", - "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", - "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "files": Object { + "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/a.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", + "testProject/b.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", + "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", + "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", + "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + }, + "hasSubmodules": false, + "hasUncommittedChanges": true, } `; -exports[`getRepoStateAsync can handle changing one file 1`] = ` +exports[`getDetailedRepoStateAsync can handle changing one file 1`] = ` Object { - "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", - "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", - "testProject/file1.txt": "f2ba8f84ab5c1bce84a7b441cb1959cfc7093b7f", - "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", - "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "files": Object { + "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", + "testProject/file1.txt": "f2ba8f84ab5c1bce84a7b441cb1959cfc7093b7f", + "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", + "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + }, + "hasSubmodules": false, + "hasUncommittedChanges": true, } `; -exports[`getRepoStateAsync can handle removing one file 1`] = ` +exports[`getDetailedRepoStateAsync can handle removing one file 1`] = ` Object { - "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", - "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", - "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", - "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "files": Object { + "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", + "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", + "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + }, + "hasSubmodules": false, + "hasUncommittedChanges": true, } `; -exports[`getRepoStateAsync can handle uncommitted filenames with spaces and non-ASCII characters 1`] = ` +exports[`getDetailedRepoStateAsync can handle uncommitted filenames with spaces and non-ASCII characters 1`] = ` Object { - "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", - "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/a file name.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", - "testProject/a file.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", - "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", - "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", - "testProject/newFile批把.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", - "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "files": Object { + "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/a file name.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", + "testProject/a file.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", + "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", + "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", + "testProject/newFile批把.txt": "2e65efe2a145dda7ee51d1741299f848e5bf752e", + "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + }, + "hasSubmodules": false, + "hasUncommittedChanges": true, } `; -exports[`getRepoStateAsync can parse committed files 1`] = ` +exports[`getDetailedRepoStateAsync can parse committed files 1`] = ` Object { - "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", - "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", - "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", - "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "files": Object { + "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", + "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", + "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + }, + "hasSubmodules": false, + "hasUncommittedChanges": false, } `; -exports[`getRepoStateAsync handles requests for additional files 1`] = ` +exports[`getDetailedRepoStateAsync handles requests for additional files 1`] = ` Object { - "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", - "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", - "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", - "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", - "testProject/log.log": "2e65efe2a145dda7ee51d1741299f848e5bf752e", - "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "files": Object { + "nestedTestProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + "nestedTestProject/src/file 1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file 2.txt": "a385f754ec4fede884a4864d090064d9aeef8ccb", + "testProject/file1.txt": "c7b2f707ac99ca522f965210a7b6b0b109863f34", + "testProject/file蝴蝶.txt": "ae814af81e16cb2ae8c57503c77e2cab6b5462ba", + "testProject/log.log": "2e65efe2a145dda7ee51d1741299f848e5bf752e", + "testProject/package.json": "18a1e415e56220fa5122428a4ef8eb8874756576", + }, + "hasSubmodules": false, + "hasUncommittedChanges": false, } `; diff --git a/libraries/package-deps-hash/src/test/getRepoDeps.test.ts b/libraries/package-deps-hash/src/test/getRepoDeps.test.ts index 1e16ff3ac5f..d81ad662e5f 100644 --- a/libraries/package-deps-hash/src/test/getRepoDeps.test.ts +++ b/libraries/package-deps-hash/src/test/getRepoDeps.test.ts @@ -4,7 +4,13 @@ import * as path from 'path'; import { execSync } from 'child_process'; -import { getRepoStateAsync, parseGitLsTree, getRepoRoot, parseGitHashObject } from '../getRepoState'; +import { + getDetailedRepoStateAsync, + type IDetailedRepoState, + parseGitLsTree, + getRepoRoot, + parseGitHashObject +} from '../getRepoState'; import { FileSystem } from '@rushstack/node-core-library'; @@ -15,20 +21,20 @@ const TEST_PROJECT_PATH: string = path.join(SOURCE_PATH, 'testProject'); const FILTERS: string[] = [`testProject/`, `nestedTestProject/`]; -function checkSnapshot(results: Map): void { +function checkSnapshot(results: IDetailedRepoState): void { const relevantResults: Record = {}; - for (const [key, hash] of results) { + for (const [key, hash] of results.files) { if (key.startsWith(TEST_PREFIX)) { const partialKey: string = key.slice(TEST_PREFIX.length); - for (const filter of FILTERS) { - if (partialKey.startsWith(filter)) { - relevantResults[partialKey] = hash; - } - } + relevantResults[partialKey] = hash; } } - expect(relevantResults).toMatchSnapshot(); + expect({ + hasSubmodules: results.hasSubmodules, + hasUncommittedChanges: results.hasUncommittedChanges, + files: relevantResults + }).toMatchSnapshot(); } describe(getRepoRoot.name, () => { @@ -113,9 +119,14 @@ describe(parseGitHashObject.name, () => { }); }); -describe(getRepoStateAsync.name, () => { +describe(getDetailedRepoStateAsync.name, () => { it('can parse committed files', async () => { - const results: Map = await getRepoStateAsync(SOURCE_PATH); + const results: IDetailedRepoState = await getDetailedRepoStateAsync( + SOURCE_PATH, + undefined, + undefined, + FILTERS + ); checkSnapshot(results); }); @@ -125,7 +136,12 @@ describe(getRepoStateAsync.name, () => { FileSystem.writeFile(tempFilePath, 'a'); try { - const results: Map = await getRepoStateAsync(SOURCE_PATH); + const results: IDetailedRepoState = await getDetailedRepoStateAsync( + SOURCE_PATH, + undefined, + undefined, + FILTERS + ); checkSnapshot(results); } finally { FileSystem.deleteFile(tempFilePath); @@ -140,7 +156,12 @@ describe(getRepoStateAsync.name, () => { FileSystem.writeFile(tempFilePath2, 'a'); try { - const results: Map = await getRepoStateAsync(SOURCE_PATH); + const results: IDetailedRepoState = await getDetailedRepoStateAsync( + SOURCE_PATH, + undefined, + undefined, + FILTERS + ); checkSnapshot(results); } finally { FileSystem.deleteFile(tempFilePath1); @@ -154,7 +175,12 @@ describe(getRepoStateAsync.name, () => { FileSystem.deleteFile(testFilePath); try { - const results: Map = await getRepoStateAsync(SOURCE_PATH); + const results: IDetailedRepoState = await getDetailedRepoStateAsync( + SOURCE_PATH, + undefined, + undefined, + FILTERS + ); checkSnapshot(results); } finally { execSync(`git checkout --force HEAD -- ${TEST_PREFIX}testProject/file1.txt`, { @@ -170,7 +196,12 @@ describe(getRepoStateAsync.name, () => { FileSystem.writeFile(testFilePath, 'abc'); try { - const results: Map = await getRepoStateAsync(SOURCE_PATH); + const results: IDetailedRepoState = await getDetailedRepoStateAsync( + SOURCE_PATH, + undefined, + undefined, + FILTERS + ); checkSnapshot(results); } finally { execSync(`git checkout --force HEAD -- ${TEST_PREFIX}testProject/file1.txt`, { @@ -190,7 +221,12 @@ describe(getRepoStateAsync.name, () => { FileSystem.writeFile(tempFilePath3, 'a'); try { - const results: Map = await getRepoStateAsync(SOURCE_PATH); + const results: IDetailedRepoState = await getDetailedRepoStateAsync( + SOURCE_PATH, + undefined, + undefined, + FILTERS + ); checkSnapshot(results); } finally { FileSystem.deleteFile(tempFilePath1); @@ -205,9 +241,12 @@ describe(getRepoStateAsync.name, () => { FileSystem.writeFile(tempFilePath1, 'a'); try { - const results: Map = await getRepoStateAsync(SOURCE_PATH, [ - `${TEST_PREFIX}testProject/log.log` - ]); + const results: IDetailedRepoState = await getDetailedRepoStateAsync( + SOURCE_PATH, + [`${TEST_PREFIX}testProject/log.log`], + undefined, + FILTERS + ); checkSnapshot(results); } finally { FileSystem.deleteFile(tempFilePath1); diff --git a/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts index 21720c29940..6219af51d54 100644 --- a/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts @@ -6,8 +6,12 @@ jest.mock(`@rushstack/package-deps-hash`, () => { getRepoRoot(dir: string): string { return dir; }, - getRepoStateAsync(): ReadonlyMap { - return new Map([['common/config/rush/npm-shrinkwrap.json', 'hash']]); + getDetailedRepoStateAsync(): IDetailedRepoState { + return { + hasSubmodules: false, + hasUncommittedChanges: false, + files: new Map([['common/config/rush/npm-shrinkwrap.json', 'hash']]) + }; }, getRepoChangesAsync(): ReadonlyMap { return new Map(); @@ -24,6 +28,7 @@ jest.mock(`@rushstack/package-deps-hash`, () => { import './mockRushCommandLineParser'; import { FileSystem, JsonFile, Path } from '@rushstack/node-core-library'; +import type { IDetailedRepoState } from '@rushstack/package-deps-hash'; import { Autoinstaller } from '../../logic/Autoinstaller'; import type { ITelemetryData } from '../../logic/Telemetry'; import { getCommandLineParserInstanceAsync } from './TestUtils'; diff --git a/libraries/rush-lib/src/cli/test/RushCommandLineParserFailureCases.test.ts b/libraries/rush-lib/src/cli/test/RushCommandLineParserFailureCases.test.ts index 387951cd5d9..4cfd7647f02 100644 --- a/libraries/rush-lib/src/cli/test/RushCommandLineParserFailureCases.test.ts +++ b/libraries/rush-lib/src/cli/test/RushCommandLineParserFailureCases.test.ts @@ -9,8 +9,12 @@ jest.mock(`@rushstack/package-deps-hash`, () => { getRepoRoot(dir: string): string { return dir; }, - getRepoStateAsync(): ReadonlyMap { - return new Map(); + getDetailedRepoStateAsync(): IDetailedRepoState { + return { + hasSubmodules: false, + hasUncommittedChanges: false, + files: new Map() + }; }, getRepoChangesAsync(): ReadonlyMap { return new Map(); @@ -19,6 +23,7 @@ jest.mock(`@rushstack/package-deps-hash`, () => { }); import { FileSystem, JsonFile } from '@rushstack/node-core-library'; +import type { IDetailedRepoState } from '@rushstack/package-deps-hash'; import { Autoinstaller } from '../../logic/Autoinstaller'; import type { ITelemetryData } from '../../logic/Telemetry'; import { getCommandLineParserInstanceAsync, setSpawnMock } from './TestUtils'; diff --git a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts index a2adc060d5b..0d1b16e9a76 100644 --- a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts +++ b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts @@ -9,7 +9,7 @@ import { Path, FileSystem, Async, AlreadyReportedError } from '@rushstack/node-c import { getRepoChanges, getRepoRoot, - getRepoStateAsync, + getDetailedRepoStateAsync, hashFilesAsync, type IFileDiffStatus } from '@rushstack/package-deps-hash'; @@ -304,8 +304,8 @@ export class ProjectChangeAnalyzer { return async function tryGetSnapshotAsync(): Promise { try { - const [hashes, additionalFiles] = await Promise.all([ - getRepoStateAsync(rootDirectory, additionalRelativePathsToHash, gitPath, filterPath), + const [{ files: hashes, hasUncommittedChanges }, additionalFiles] = await Promise.all([ + getDetailedRepoStateAsync(rootDirectory, additionalRelativePathsToHash, gitPath, filterPath), getAdditionalFilesFromRushProjectConfigurationAsync( additionalGlobs, lookupByPath, @@ -328,8 +328,9 @@ export class ProjectChangeAnalyzer { additionalHashes, globalAdditionalFiles, hashes, + hasUncommittedChanges, lookupByPath, - projectMap: projectMap, + projectMap, rootDir: rootDirectory }); } catch (e) { diff --git a/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts b/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts index 94c31ea19c3..8eeaa5ed224 100644 --- a/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts +++ b/libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts @@ -98,6 +98,10 @@ export interface IInputsSnapshotParameters { * The hashes of all tracked files in the repository. */ hashes: ReadonlyMap; + /** + * Whether or not the repository has uncommitted changes. + */ + hasUncommittedChanges: boolean; /** * Optimized lookup engine used to route `hashes` to individual projects. */ @@ -131,6 +135,11 @@ export interface IInputsSnapshot { */ readonly rootDirectory: string; + /** + * Whether or not the repository has uncommitted changes. + */ + readonly hasUncommittedChanges: boolean; + /** * Gets the map of file paths to Git hashes that will be used to compute the local state hash of the operation. * Exposed separately from the final state hash to facilitate detailed change detection. @@ -168,6 +177,10 @@ export class InputsSnapshot implements IInputsSnapshot { * {@inheritdoc IInputsSnapshot.hashes} */ public readonly hashes: ReadonlyMap; + /** + * {@inheritdoc IInputsSnapshot.hasUncommittedChanges} + */ + public readonly hasUncommittedChanges: boolean; /** * {@inheritdoc IInputsSnapshot.rootDirectory} */ @@ -204,6 +217,7 @@ export class InputsSnapshot implements IInputsSnapshot { environment = { ...process.env }, globalAdditionalFiles, hashes, + hasUncommittedChanges, lookupByPath, rootDir } = params; @@ -261,6 +275,7 @@ export class InputsSnapshot implements IInputsSnapshot { // Snapshot the environment so that queries are not impacted by when they happen this._environment = environment; this.hashes = hashes; + this.hasUncommittedChanges = hasUncommittedChanges; this.rootDirectory = rootDir; } diff --git a/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts b/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts index a082fbc5098..867a1f446f9 100644 --- a/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts +++ b/libraries/rush-lib/src/logic/incremental/test/InputsSnapshot.test.ts @@ -31,6 +31,7 @@ describe(InputsSnapshot.name, () => { ['a/lib/file3.js', 'hash3'], ['common/config/some-config.json', 'hash5'] ]), + hasUncommittedChanges: false, lookupByPath: new LookupByPath([[project.projectRelativeFolder, project]]), projectMap: new Map() } diff --git a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts index fc9e983494e..9ab196ea8a8 100644 --- a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts +++ b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts @@ -26,8 +26,12 @@ jest.mock(`@rushstack/package-deps-hash`, () => { getRepoRoot(dir: string): string { return dir; }, - getRepoStateAsync(): ReadonlyMap { - return mockHashes; + getDetailedRepoStateAsync(): IDetailedRepoState { + return { + hasSubmodules: false, + hasUncommittedChanges: false, + files: mockHashes + }; }, getRepoChangesAsync(): ReadonlyMap { return new Map(); @@ -51,6 +55,7 @@ jest.mock('../incremental/InputsSnapshot', () => { import { resolve } from 'node:path'; +import type { IDetailedRepoState } from '@rushstack/package-deps-hash'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; import { ProjectChangeAnalyzer } from '../ProjectChangeAnalyzer';