Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(release): Support path offset from git root to Nx workspace #28168

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/js/src/generators/release-version/release-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ProjectsVersionPlan,
} from 'nx/src/command-line/release/config/version-plans';
import {
getAbsoluteGitRoot,
getFirstGitCommit,
getLatestGitTagForPattern,
} from 'nx/src/command-line/release/utils/git';
Expand All @@ -34,6 +35,7 @@ import {
deriveNewSemverVersion,
validReleaseVersionPrefixes,
} from 'nx/src/command-line/release/version';
import { getWorkspaceGitRootOffset } from 'nx/src/command-line/release/utils/shared';
import { interpolate } from 'nx/src/tasks-runner/utils';
import * as ora from 'ora';
import { ReleaseType, gt, inc, prerelease } from 'semver';
Expand Down Expand Up @@ -408,11 +410,18 @@ To fix this you will either need to add a package.json file at that location, or
);
}

// Obtain path offset from git root to Nx workspace for later normalizations
const workspaceGitRootOffset = getWorkspaceGitRootOffset(
await getAbsoluteGitRoot(),
workspaceRoot
);

specifier = await resolveSemverSpecifierFromConventionalCommits(
previousVersionRef,
options.projectGraph,
affectedProjects,
options.conventionalCommitsConfig
options.conventionalCommitsConfig,
workspaceGitRootOffset
);

if (!specifier) {
Expand Down
39 changes: 30 additions & 9 deletions packages/nx/src/command-line/release/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
import {
GitCommit,
Reference,
getAbsoluteGitRoot,
getCommitHash,
getFirstGitCommit,
getGitDiff,
Expand All @@ -71,6 +72,7 @@ import {
commitChanges,
createCommitMessageValues,
createGitTagValues,
getWorkspaceGitRootOffset,
handleDuplicateGitTags,
noDiffInChangelogMessage,
} from './utils/shared';
Expand Down Expand Up @@ -273,6 +275,12 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
// If there are multiple release groups, we'll just skip the workspace changelog anyway.
const versionPlansEnabledForWorkspaceChangelog =
releaseGroups[0].resolvedVersionPlans;
// Obtain path offset from git root to Nx workspace for later normalizations
const workspaceGitRootOffset = getWorkspaceGitRootOffset(
await getAbsoluteGitRoot(),
workspaceRoot
);

if (versionPlansEnabledForWorkspaceChangelog) {
if (releaseGroups.length === 1) {
const releaseGroup = releaseGroups[0];
Expand All @@ -287,7 +295,7 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
let githubReferences = [];
let author = undefined;
const parsedCommit = vp.commit
? parseGitCommit(vp.commit, true)
? parseGitCommit(vp.commit, { isVersionPlanCommit: true })
: null;
if (parsedCommit) {
githubReferences = parsedCommit.references;
Expand Down Expand Up @@ -351,7 +359,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {

workspaceChangelogCommits = await getCommits(
workspaceChangelogFromSHA,
toSHA
toSHA,
workspaceGitRootOffset
);

workspaceChangelogChanges = filterHiddenChanges(
Expand Down Expand Up @@ -518,7 +527,7 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
let githubReferences = [];
let authors = [];
const parsedCommit = vp.commit
? parseGitCommit(vp.commit, true)
? parseGitCommit(vp.commit, { isVersionPlanCommit: true })
: null;
if (parsedCommit) {
githubReferences = parsedCommit.references;
Expand Down Expand Up @@ -552,7 +561,11 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {

if (!fromRef && useAutomaticFromRef) {
const firstCommit = await getFirstGitCommit();
const allCommits = await getCommits(firstCommit, toSHA);
const allCommits = await getCommits(
firstCommit,
toSHA,
workspaceGitRootOffset
);
const commitsForProject = allCommits.filter((c) =>
c.affectedFiles.find((f) => f.startsWith(project.data.root))
);
Expand All @@ -573,7 +586,11 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
}

if (!commits) {
commits = await getCommits(fromRef, toSHA);
commits = await getCommits(
fromRef,
toSHA,
workspaceGitRootOffset
);
}

const { fileMap } = await createFileMapUsingProjectGraph(
Expand Down Expand Up @@ -670,7 +687,7 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
let githubReferences = [];
let author = undefined;
const parsedCommit = vp.commit
? parseGitCommit(vp.commit, true)
? parseGitCommit(vp.commit, { isVersionPlanCommit: true })
: null;
if (parsedCommit) {
githubReferences = parsedCommit.references;
Expand Down Expand Up @@ -735,7 +752,7 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
fileMap.projectFileMap
);

commits = await getCommits(fromSHA, toSHA);
commits = await getCommits(fromSHA, toSHA, workspaceGitRootOffset);
changes = filterHiddenChanges(
commits.map((c) => ({
type: c.type,
Expand Down Expand Up @@ -1354,11 +1371,15 @@ function checkChangelogFilesEnabled(nxReleaseConfig: NxReleaseConfig): boolean {

async function getCommits(
fromSHA: string,
toSHA: string
toSHA: string,
gitRootToWorkspacePath?: string
): Promise<GitCommit[]> {
const rawCommits = await getGitDiff(fromSHA, toSHA);
// Parse as conventional commits
return parseCommits(rawCommits);
return parseCommits(rawCommits, {
isVersionPlanCommit: false,
gitRootToWorkspacePath,
});
}

function filterHiddenChanges(
Expand Down
48 changes: 38 additions & 10 deletions packages/nx/src/command-line/release/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,13 @@ export async function gitPush({
}
}

export function parseCommits(commits: RawGitCommit[]): GitCommit[] {
return commits.map((commit) => parseGitCommit(commit)).filter(Boolean);
export function parseCommits(
commits: RawGitCommit[],
options: ParseGitCommitOptions = {}
): GitCommit[] {
return commits
.map((commit) => parseGitCommit(commit, options))
.filter(Boolean);
}

export function parseConventionalCommitsMessage(message: string): {
Expand Down Expand Up @@ -453,12 +458,17 @@ const IssueRE = /(#\d+)/gm;
const ChangedFileRegex = /(A|M|D|R\d*|C\d*)\t([^\t\n]*)\t?(.*)?/gm;
const RevertHashRE = /This reverts commit (?<hash>[\da-f]{40})./gm;

export interface ParseGitCommitOptions {
readonly isVersionPlanCommit?: boolean;
readonly gitRootToWorkspacePath?: string;
}

export function parseGitCommit(
commit: RawGitCommit,
isVersionPlanCommit = false
options: ParseGitCommitOptions = {}
): GitCommit | null {
// For version plans, we do not require conventional commits and therefore cannot extract data based on that format
if (isVersionPlanCommit) {
if (options.isVersionPlanCommit) {
return {
...commit,
description: commit.message,
Expand Down Expand Up @@ -513,15 +523,25 @@ export function parseGitCommit(
// Find all authors
const authors = getAllAuthorsForCommit(commit);

const replaceGitRootPathDiff = (file: string) => {
if (
!options.gitRootToWorkspacePath ||
!file.startsWith(options.gitRootToWorkspacePath)
) {
return file;
}

return file.replace(options.gitRootToWorkspacePath, '');
};

// Extract file changes from commit body
const affectedFiles = Array.from(
commit.body.matchAll(ChangedFileRegex)
).reduce(
(prev, [fullLine, changeType, file1, file2]: RegExpExecArray) =>
// file2 only exists for some change types, such as renames
file2 ? [...prev, file1, file2] : [...prev, file1],
[] as string[]
);
).reduce((prev, [fullLine, changeType, file1, file2]: RegExpExecArray) => {
// file2 only exists for some change types, such as renames
const files = file2 ? [file1, file2] : [file1];
return [...prev, ...files.map((file) => replaceGitRootPathDiff(file))];
}, [] as string[]);

return {
...commit,
Expand All @@ -536,6 +556,14 @@ export function parseGitCommit(
};
}

export async function getAbsoluteGitRoot() {
try {
return (await execCommand('git', ['rev-parse', '--show-toplevel'])).trim();
} catch (e) {
throw new Error(`Could not determine git root`);
}
}

export async function getCommitHash(ref: string) {
try {
return (await execCommand('git', ['rev-parse', ref])).trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ export async function resolveSemverSpecifierFromConventionalCommits(
from: string,
projectGraph: ProjectGraph,
projectNames: string[],
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits']
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'],
gitRootToWorkspacePath?: string
): Promise<string | null> {
const commits = await getGitDiff(from);
const parsedCommits = parseCommits(commits);
const parsedCommits = parseCommits(commits, {
isVersionPlanCommit: false,
gitRootToWorkspacePath,
});
const relevantCommits = await getCommitsRelevantToProjects(
projectGraph,
parsedCommits,
Expand Down
50 changes: 49 additions & 1 deletion packages/nx/src/command-line/release/utils/shared.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ReleaseGroupWithName } from '../config/filter-release-groups';
import { createCommitMessageValues, createGitTagValues } from './shared';
import {
createCommitMessageValues,
createGitTagValues,
getWorkspaceGitRootOffset,
} from './shared';

describe('shared', () => {
describe('createCommitMessageValues()', () => {
Expand Down Expand Up @@ -278,4 +282,48 @@ describe('shared', () => {
return { releaseGroup, releaseGroupToFilteredProjects };
}
});

describe('getWorkspaceGitRootOffset', () => {
it('should return undefined when gitRoot and workspaceRoot are the same', () => {
const gitRoot = '/home/test/project';
const workspaceRoot = '/home/test/project';
const result = getWorkspaceGitRootOffset(gitRoot, workspaceRoot, '/');
expect(result).toBeUndefined();
});

it('should return the correct offset when workspaceRoot has a subdirectory', () => {
const gitRoot = '/home/test/project';
const workspaceRoot = '/home/test/project/workspace';
const result = getWorkspaceGitRootOffset(gitRoot, workspaceRoot, '/');
expect(result).toBe('workspace/');
});

it('should return undefined when gitRoot and workspaceRoot are the same in a Windows path', () => {
const gitRoot = 'C:\\home\\test\\project';
const workspaceRoot = 'C:\\home\\test\\project';
const result = getWorkspaceGitRootOffset(gitRoot, workspaceRoot, '\\');
expect(result).toBeUndefined();
});

it('should return the correct offset when workspaceRoot has a subdirectory in a Windows path', () => {
const gitRoot = 'C:\\home\\test\\project';
const workspaceRoot = 'C:\\home\\test\\project\\workspace';
const result = getWorkspaceGitRootOffset(gitRoot, workspaceRoot, '\\');
expect(result).toBe('workspace\\');
});

it('should handle complex subdirectory structures correctly', () => {
const gitRoot = '/home/test/project';
const workspaceRoot = '/home/test/project/workspace/subdir';
const result = getWorkspaceGitRootOffset(gitRoot, workspaceRoot, '/');
expect(result).toBe('workspace/subdir/');
});

it('should handle complex subdirectory structures in Windows paths correctly', () => {
const gitRoot = 'C:\\home\\test\\project';
const workspaceRoot = 'C:\\home\\test\\project\\workspace\\subdir';
const result = getWorkspaceGitRootOffset(gitRoot, workspaceRoot, '\\');
expect(result).toBe('workspace\\subdir\\');
});
});
});
14 changes: 14 additions & 0 deletions packages/nx/src/command-line/release/utils/shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as chalk from 'chalk';
import * as path from 'path';
import { prerelease } from 'semver';
import { ProjectGraph } from '../../../config/project-graph';
import { Tree } from '../../../generators/tree';
Expand Down Expand Up @@ -337,3 +338,16 @@ export async function getCommitsRelevantToProjects(
)
);
}

export function getWorkspaceGitRootOffset(
gitRoot: string,
workspaceRoot: string,
separator = path.sep
): string | undefined {
// Slice the gitRoot part from workspaceRoot to get the relative path
const offset = workspaceRoot.slice(gitRoot.length);
// Ensure there is a valid relative path and return it with the appropriate separator
return offset.startsWith(separator) && offset.length > 1
? offset.slice(1) + separator
: undefined;
}