diff --git a/packages/nx/src/command-line/release/changelog.ts b/packages/nx/src/command-line/release/changelog.ts index a87e329a5d16f9..ed2f4913d917c7 100644 --- a/packages/nx/src/command-line/release/changelog.ts +++ b/packages/nx/src/command-line/release/changelog.ts @@ -370,7 +370,8 @@ export function createAPI( preid: string | undefined, checkAllBranchesWhen: CheckAllBranchesWhen, requireSemver: boolean, - strictPreid: boolean + strictPreid: boolean, + projectRoot?: string ): Promise => { if (fromSHACache.has(cacheKey)) { return fromSHACache.get(cacheKey); @@ -386,6 +387,7 @@ export function createAPI( requireSemver, strictPreid, useAutomaticFromRef, + projectRoot, }); fromSHACache.set(cacheKey, sha); return sha; @@ -491,7 +493,8 @@ export function createAPI( projectsPreid[project.name], releaseGroup.releaseTag.checkAllBranchesWhen, releaseGroup.releaseTag.requireSemver, - releaseGroup.releaseTag.strictPreid + releaseGroup.releaseTag.strictPreid, + project.data.root ); let commits: GitCommit[]; diff --git a/packages/nx/src/command-line/release/changelog/version-plan-filtering.ts b/packages/nx/src/command-line/release/changelog/version-plan-filtering.ts index e64cbeae4b675f..84e408f4e6235e 100644 --- a/packages/nx/src/command-line/release/changelog/version-plan-filtering.ts +++ b/packages/nx/src/command-line/release/changelog/version-plan-filtering.ts @@ -8,6 +8,7 @@ import { execCommand } from '../utils/exec-command'; import { getCommitHash, getFirstGitCommit, + getFirstProjectCommit, getLatestGitTagForPattern, } from '../utils/git'; import type { VersionData } from '../utils/shared'; @@ -133,6 +134,7 @@ export async function resolveChangelogFromSHA({ strictPreid, useAutomaticFromRef, resolveRepositoryTags, + projectRoot, }: { fromRef?: string; tagPattern: string; @@ -143,6 +145,8 @@ export async function resolveChangelogFromSHA({ strictPreid: boolean; useAutomaticFromRef: boolean; resolveRepositoryTags: RepoGitTags['resolveTags']; + /** When provided, scopes the fallback to the project's first commit instead of the repo's first commit */ + projectRoot?: string; }): Promise { // If user provided a from ref, resolve it to a SHA if (fromRef) { @@ -163,9 +167,13 @@ export async function resolveChangelogFromSHA({ if (latestTag?.tag) { return await getCommitHash(latestTag.tag); } - // Finally, if automatic from ref is enabled, use the first commit as a fallback + // Finally, if automatic from ref is enabled, use the first commit as a fallback. + // When a projectRoot is provided, scope the fallback to the project's first commit + // to avoid scanning the entire repo history for projects added after the repo was created. if (useAutomaticFromRef) { - return await getFirstGitCommit(); + return projectRoot + ? await getFirstProjectCommit(projectRoot) + : await getFirstGitCommit(); } return null; diff --git a/packages/nx/src/command-line/release/utils/git.ts b/packages/nx/src/command-line/release/utils/git.ts index f7982b0ea3c224..a0972ed9c15f84 100644 --- a/packages/nx/src/command-line/release/utils/git.ts +++ b/packages/nx/src/command-line/release/utils/git.ts @@ -732,6 +732,44 @@ export async function getFirstGitCommit() { } } +/** + * Returns the parent of the first commit that touched the given project root, + * so that `from..HEAD` ranges include the project's creation commit. + * Falls back to getFirstGitCommit() if the project history cannot be determined. + */ +export async function getFirstProjectCommit( + projectRoot: string +): Promise { + try { + const result = ( + await execCommand('git', [ + 'rev-list', + '--reverse', + 'HEAD', + '--first-parent', + '--', + `${projectRoot}/package.json`, + ]) + ).trim(); + const firstCommit = result.split('\n')[0]; + + if (firstCommit) { + // Return the parent so the creation commit is included in from..to ranges + try { + return ( + await execCommand('git', ['rev-parse', `${firstCommit}~1`]) + ).trim(); + } catch { + // No parent (project was added in the repo's very first commit) + return firstCommit; + } + } + } catch { + // fall through to fallback + } + return getFirstGitCommit(); +} + async function getGitRoot() { try { return (await execCommand('git', ['rev-parse', '--show-toplevel'])).trim(); diff --git a/packages/nx/src/command-line/release/version/derive-specifier-from-conventional-commits.ts b/packages/nx/src/command-line/release/version/derive-specifier-from-conventional-commits.ts index ccb619062dc630..e5e0eac26380ef 100644 --- a/packages/nx/src/command-line/release/version/derive-specifier-from-conventional-commits.ts +++ b/packages/nx/src/command-line/release/version/derive-specifier-from-conventional-commits.ts @@ -4,7 +4,11 @@ import type { } from '../../../config/project-graph'; import { NxReleaseConfig } from '../config/config'; import { ReleaseGroupWithName } from '../config/filter-release-groups'; -import { getFirstGitCommit, getLatestGitTagForPattern } from '../utils/git'; +import { + getFirstGitCommit, + getFirstProjectCommit, + getLatestGitTagForPattern, +} from '../utils/git'; import { ReleaseGraph } from '../utils/release-graph'; import { resolveSemverSpecifierFromConventionalCommits } from '../utils/resolve-semver-specifier'; import { SemverSpecifier, SemverSpecifierType } from '../utils/semver'; @@ -36,11 +40,12 @@ export async function deriveSpecifierFromConventionalCommits( : releaseGroup.projects; // latestMatchingGitTag will be undefined if the current version was resolved from the disk fallback. - // In this case, we want to use the first commit as the ref to be consistent with the changelog command. + // In this case, use the first commit that touched this project rather than the repo's first commit, + // to avoid scanning the entire git history for projects that were added after the repo was created. const previousVersionRef = latestMatchingGitTag ? latestMatchingGitTag.tag : fallbackCurrentVersionResolver === 'disk' - ? await getFirstGitCommit() + ? await getFirstProjectCommit(projectGraphNode.data.root) : undefined; if (!previousVersionRef) {