Skip to content

Commit

Permalink
feat: handle multiple commits in a single message (#2358)
Browse files Browse the repository at this point in the history
* feat: handle multiple commits in a single message

- updated `splitMessages` function to handle multiple conventional commits within the main message
- added logic to separate conventional commits (feat, fix, docs, etc.) within the main message
- preserved the original message structure outside of nested commit blocks
- enhanced breaking change detection from commit body in `toConventionalChangelogFormat` and `postProcessCommits` functions

Resolves #2357

* test: new test case for parsing multiple commits

- added new test cases for parsing multiple commits from a single message
- added fixture file for multiple commits in a single message
- changed the order of the expected commits in `multiple-messages` and `1257-breaking-change` fixtures since the order of the commits in the is not reversed anymore

* test: remove duplicate test for parsing multiple commits
  • Loading branch information
dgokcin authored Sep 17, 2024
1 parent 8556500 commit ec41c38
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 13 deletions.
54 changes: 47 additions & 7 deletions src/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}

Expand All @@ -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)];
}

/**
Expand Down
24 changes: 18 additions & 6 deletions test/commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down Expand Up @@ -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')];
Expand Down Expand Up @@ -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 () => {
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/commit-messages/multiple-commits-single-message.txt
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ec41c38

Please sign in to comment.