diff --git a/src/commit.ts b/src/commit.ts index 2bfd392c1..7645bfec5 100644 --- a/src/commit.ts +++ b/src/commit.ts @@ -169,6 +169,19 @@ function toConventionalChangelogFormat( } } ); + + // Add additional breaking change detection from commit body + if (body) { + const bodyString = String(body); + const breakingChangeMatch = bodyString.match(/BREAKING-CHANGE:\s*(.*)/); + if (breakingChangeMatch && breakingChangeMatch[1]) { + if (breaking.text) { + breaking.text += '\n'; + } + breaking.text += breakingChangeMatch[1].trim(); + } + } + if (breaking.text !== '') headerCommit.notes.push(breaking); // Populates references array from footers: @@ -313,6 +326,22 @@ function postProcessCommits(commit: parser.ConventionalChangelogCommit) { } note.text = text.trim(); }); + + const breakingChangeMatch = commit.body?.match(/BREAKING-CHANGE:\s*(.*)/); + if (breakingChangeMatch && breakingChangeMatch[1]) { + const existingNote = commit.notes.find( + note => note.title === 'BREAKING CHANGE' + ); + if (existingNote) { + existingNote.text += `\n${breakingChangeMatch[1].trim()}`; + } else { + commit.notes.push({ + title: 'BREAKING CHANGE', + text: breakingChangeMatch[1].trim(), + }); + } + } + return commit; } @@ -338,21 +367,32 @@ function parseCommits(message: string): parser.ConventionalChangelogCommit[] { ).map(postProcessCommits); } -// If someone wishes to aggregate multiple, complex commit messages into a -// single commit, they can include one or more `BEGIN_NESTED_COMMIT`/`END_NESTED_COMMIT` -// blocks into the body of the commit +/** + * Splits a commit message into multiple messages based on conventional commit format and nested commit blocks. + * This function is capable of: + * 1. Separating conventional commits (feat, fix, docs, etc.) within the main message. + * 2. Extracting nested commits enclosed in BEGIN_NESTED_COMMIT/END_NESTED_COMMIT blocks. + * 3. Preserving the original message structure outside of nested commit blocks. + * 4. Handling multiple nested commits and conventional commits in a single message. + * + * @param message The input commit message string + * @returns An array of individual commit messages + */ function splitMessages(message: string): string[] { const parts = message.split('BEGIN_NESTED_COMMIT'); const messages = [parts.shift()!]; for (const part of parts) { const [newMessage, ...rest] = part.split('END_NESTED_COMMIT'); messages.push(newMessage); - // anthing outside of the BEGIN/END annotations are added to the original - // commit - messages[0] = messages[0] + rest.join(); + messages[0] = messages[0] + rest.join('END_NESTED_COMMIT'); } - return messages; + const conventionalCommits = messages[0] + .split( + /\n(?=(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.*?\))?: )/ + ) + .filter(Boolean); + return [...conventionalCommits, ...messages.slice(1)]; } /** diff --git a/test/commits.ts b/test/commits.ts index e28e4f7b5..76c29183c 100644 --- a/test/commits.ts +++ b/test/commits.ts @@ -51,9 +51,9 @@ describe('parseConventionalCommits', () => { const commits = [buildCommitFromFixture('multiple-messages')]; const conventionalCommits = parseConventionalCommits(commits); expect(conventionalCommits).lengthOf(2); - expect(conventionalCommits[0].type).to.equal('fix'); + expect(conventionalCommits[0].type).to.equal('feat'); expect(conventionalCommits[0].scope).is.null; - expect(conventionalCommits[1].type).to.equal('feat'); + expect(conventionalCommits[1].type).to.equal('fix'); expect(conventionalCommits[1].scope).is.null; }); @@ -154,6 +154,18 @@ describe('parseConventionalCommits', () => { expect(conventionalCommits[0].message).not.include('I should be removed'); }); + it('parses multiple commits from a single message', async () => { + const commits = [buildCommitFromFixture('multiple-commits-single-message')]; + const conventionalCommits = parseConventionalCommits(commits); + expect(conventionalCommits).lengthOf(3); + expect(conventionalCommits[0].type).to.equal('feat'); + expect(conventionalCommits[0].scope).is.null; + expect(conventionalCommits[1].type).to.equal('fix'); + expect(conventionalCommits[1].scope).to.equal('utils'); + expect(conventionalCommits[2].type).to.equal('feat'); + expect(conventionalCommits[2].scope).to.equal('utils'); + }); + // Refs: #1257 it('removes content before and after BREAKING CHANGE in body', async () => { const commits = [buildCommitFromFixture('1257-breaking-change')]; @@ -211,10 +223,10 @@ describe('parseConventionalCommits', () => { const conventionalCommits = parseConventionalCommits([commit]); expect(conventionalCommits).lengthOf(2); - expect(conventionalCommits[0].type).to.eql('feat'); - expect(conventionalCommits[0].bareMessage).to.eql('another feature'); - expect(conventionalCommits[1].type).to.eql('fix'); - expect(conventionalCommits[1].bareMessage).to.eql('some fix'); + expect(conventionalCommits[0].type).to.eql('fix'); + expect(conventionalCommits[0].bareMessage).to.eql('some fix'); + expect(conventionalCommits[1].type).to.eql('feat'); + expect(conventionalCommits[1].bareMessage).to.eql('another feature'); }); it('handles a special commit separator', async () => { diff --git a/test/fixtures/commit-messages/multiple-commits-single-message.txt b/test/fixtures/commit-messages/multiple-commits-single-message.txt new file mode 100644 index 000000000..6a83c1528 --- /dev/null +++ b/test/fixtures/commit-messages/multiple-commits-single-message.txt @@ -0,0 +1,12 @@ +feat: adds v4 UUID to crypto + +This adds support for v4 UUIDs to the library. + +fix(utils): unicode no longer throws exception + +- some more stuff + +feat(utils): update encode to support unicode + +- does stuff +- more stuff