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

feat: handle multiple commits in a single message #2358

Merged
merged 3 commits into from
Sep 17, 2024
Merged
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
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
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
Loading