Repository: refined-github/refined-github
Pull request #7804 (2024-09-12 01:16:57)

`default-branch-button` - Highlight non default branch button
Closes: #7679 


## Test URLs

https://github.com/refined-github/refined-github/tree/sandbox/keep-branch

## Screenshot

<img width="937" alt="image" src="https://github.com/user-attachments/assets/881a53fd-9765-4769-8e7a-c16dfd06a6c4">

<img width="914" alt="image" src="https://github.com/user-attachments/assets/1485adb1-30d4-4cb3-b6a8-97e0a016709a">

<img width="917" alt="image" src="https://github.com/user-attachments/assets/ac0e084d-4f6e-4176-b0b4-4ad507453845">

<img width="926" alt="image" src="https://github.com/user-attachments/assets/99b630dd-9e40-4e78-9402-5da9ecfb71c8">



Patch:
diff --git a/build/__snapshots__/features-meta.json b/build/__snapshots__/features-meta.json
index 4b5b778e..c9de2ed6 100644
--- a/build/__snapshots__/features-meta.json
+++ b/build/__snapshots__/features-meta.json
@@ -147,6 +147,12 @@
 		"css": true,
 		"screenshot": "https://github-production-user-asset-6210df.s3.amazonaws.com/83146190/253128438-d67c8f49-44f1-4e15-9363-a717109fef39.png"
 	},
+	{
+		"id": "conventional-commits",
+		"description": "Shows conventional commit types as labels before the commit message.",
+		"css": true,
+		"screenshot": "https://github.com/user-attachments/assets/980a2d5e-13c2-4b1b-bb80-81dc94723000"
+	},
 	{
 		"id": "conversation-activity-filter",
 		"description": "Lets you hide every event except comments or unresolved comments in issues and PRs.",
@@ -588,7 +594,7 @@
 	},
 	{
 		"id": "pr-branch-auto-delete",
-		"description": "Automatically deletes the branch right after merging a PR, if possible.",
+		"description": "Automatically deletes the branch right after merging a PR, if possible. Common branch names known to be long-lived are excluded (<code>develop</code>, <code>release/*</code>, etc)",
 		"screenshot": "https://user-images.githubusercontent.com/1402241/177067141-eabc7494-38a2-45b5-aef9-ac33cc0da370.png"
 	},
 	{
diff --git a/build/__snapshots__/imported-features.json b/build/__snapshots__/imported-features.json
index 70e2ae3d..6c188036 100644
--- a/build/__snapshots__/imported-features.json
+++ b/build/__snapshots__/imported-features.json
@@ -27,6 +27,7 @@
 	"comment-excess",
 	"comments-time-machine-links",
 	"conflict-marker",
+	"conventional-commits",
 	"conversation-activity-filter",
 	"conversation-links-on-repo-lists",
 	"convert-pr-to-draft-improvements",
diff --git a/build/readme-parser.ts b/build/readme-parser.ts
index 88f0c887..5e81d93f 100644
--- a/build/readme-parser.ts
+++ b/build/readme-parser.ts
@@ -10,7 +10,8 @@ const highlightedFeatureRegex = /<p><a title="(?<highlightedId>[^"]+)"><\/a> (?<
 const featureRegex = regexJoinWithSeparator('|', [simpleFeatureRegex, highlightedFeatureRegex]);
 const imageRegex = /\.\w{3}$/; // 3 since .png and .gif have 3 letters
 const rghUploadsRegex = /refined-github[/]refined-github[/]assets[/]/;
-const screenshotRegex = regexJoinWithSeparator('|', [imageRegex, rghUploadsRegex]);
+const userAttachmentsRegex = /user-attachments[/]assets[/]/;
+const screenshotRegex = regexJoinWithSeparator('|', [imageRegex, rghUploadsRegex, userAttachmentsRegex]);
 
 function extractDataFromMatch(match: RegExpMatchArray): FeatureMeta {
 	const {
diff --git a/readme.md b/readme.md
index a8bf173c..3b1c41e9 100644
--- a/readme.md
+++ b/readme.md
@@ -264,7 +264,7 @@ Thanks for contributing! 🦋🙌
 - [](# "update-pr-from-base-branch") [Adds an "Update branch" button to every PR.](https://user-images.githubusercontent.com/1402241/234483592-4867cb2e-21cb-436d-9ea0-aedadf834f19.png) GitHub has the same feature, but it must be manually configured with protected branches.
 - [](# "one-click-review-submission") [Simplifies the PR review form: Approve or reject reviews faster with one-click review-type buttons.](https://user-images.githubusercontent.com/1402241/236627732-df341ff7-cd98-4cd0-a579-722d1fffa5cf.png)
 - [](# "pull-request-hotkeys") [Adds keyboard shortcuts to cycle through PR tabs: <kbd>g</kbd> <kbd>←</kbd> and <kbd>g</kbd> <kbd>→</kbd>, or <kbd>g</kbd> <kbd>1</kbd>, <kbd>g</kbd> <kbd>2</kbd>, <kbd>g</kbd> <kbd>3</kbd>, and <kbd>g</kbd> <kbd>4</kbd>](https://user-images.githubusercontent.com/16872793/94634958-7e7b5680-029f-11eb-82ea-1f96cd11e4cd.png).
-- [](# "pr-branch-auto-delete") [Automatically deletes the branch right after merging a PR, if possible.](https://user-images.githubusercontent.com/1402241/177067141-eabc7494-38a2-45b5-aef9-ac33cc0da370.png)
+- [](# "pr-branch-auto-delete") [Automatically deletes the branch right after merging a PR, if possible.](https://user-images.githubusercontent.com/1402241/177067141-eabc7494-38a2-45b5-aef9-ac33cc0da370.png) Common branch names known to be long-lived are excluded (`develop`, `release/*`, etc)
 - [](# "one-click-pr-or-gist") [Lets you create draft PRs and public gists in one click.](https://user-images.githubusercontent.com/34235681/152473201-868ad7c1-e06f-4826-b808-d90bca7f08b3.png)
 - [](# "clear-pr-merge-commit-message") [Clears the PR merge commit message of clutter, leaving only deduplicated co-authors.](https://user-images.githubusercontent.com/1402241/79257078-62b6fc00-7e89-11ea-8798-c06f33baa94b.png)
 - [](# "quick-review") [Adds quick-review buttons to the PR sidebar, automatically focuses the review textarea, and adds a keyboard shortcut to open the review popup: <kbd>v</kbd>.](https://github.com/refined-github/refined-github/assets/1402241/f11039c4-c9d1-4adc-9a65-cfe1f2027ec3)
@@ -288,6 +288,7 @@ Thanks for contributing! 🦋🙌
 - [](# "same-branch-author-commits") [Preserves current branch and path when viewing all commits by an author.](https://user-images.githubusercontent.com/44045911/148764372-ee443213-e61a-4227-9219-0ee54ed832e8.png)
 - [](# "easy-toggle-commit-messages") [Enables toggling commit messages by clicking on the commit box.](https://user-images.githubusercontent.com/1402241/152121837-ca13bf8a-9b7f-4517-8e8d-b58bb135523b.gif)
 - [](# "link-to-compare-diff") [Linkifies the "X files changed" text on compare pages to allow jumping to the diff.](https://user-images.githubusercontent.com/46634000/157072587-0335357a-18c7-44c4-ae6e-237080fb36b4.png)
+- [](# "conventional-commits") [Shows conventional commit types as labels before the commit message.](https://github.com/user-attachments/assets/980a2d5e-13c2-4b1b-bb80-81dc94723000)
 
 <!-- Refer to style guide above. Keep this message between sections. -->
 
diff --git a/source/features/clean-conversation-headers.tsx b/source/features/clean-conversation-headers.tsx
index 82336713..3ad09829 100644
--- a/source/features/clean-conversation-headers.tsx
+++ b/source/features/clean-conversation-headers.tsx
@@ -23,7 +23,11 @@ async function cleanPrHeader(byline: HTMLElement): Promise<void> {
 
 	// Extra author name is only shown on `isPRConversation`
 	// Hide if it's the same as the opener (always) or merger
-	const shouldHideAuthor = pageDetect.isPRConversation() && $('.author', byline)!.textContent === (await elementReady('.TimelineItem .author'))!.textContent;
+	const shouldHideAuthor
+		= pageDetect.isPRConversation()
+		&& !byline.closest('.gh-header-sticky') // #7802
+		&& $('.author', byline)!.textContent === (await elementReady('.TimelineItem .author'))!.textContent;
+
 	if (shouldHideAuthor) {
 		byline.classList.add('rgh-clean-conversation-headers-hide-author');
 	}
diff --git a/source/features/conventional-commits.css b/source/features/conventional-commits.css
new file mode 100644
index 00000000..a5365b5d
--- /dev/null
+++ b/source/features/conventional-commits.css
@@ -0,0 +1,89 @@
+:root [rgh-conventional-commits] {
+	font-size: var(--body-font-size, 1px);
+	line-height: 1.5;
+}
+
+/* Color from `enhancement` label */
+[rgh-conventional-commits='feat'] {
+	--label-r: 162;
+	--label-g: 238;
+	--label-b: 239;
+	--label-h: 180;
+	--label-s: 70;
+	--label-l: 78;
+}
+
+/* Color from `bug` label */
+[rgh-conventional-commits='fix'] {
+	--label-r: 215;
+	--label-g: 58;
+	--label-b: 74;
+	--label-h: 353;
+	--label-s: 66;
+	--label-l: 53;
+}
+
+/* Color from `wontfix` label. Just white because it's the most common and meaningless type of commit */
+[rgh-conventional-commits='chore'] {
+	--label-r: 255;
+	--label-g: 255;
+	--label-b: 255;
+	--label-h: 0;
+	--label-s: 0;
+	--label-l: 100;
+}
+
+/* Color from `documentation` label */
+[rgh-conventional-commits='docs'] {
+	--label-r: 0;
+	--label-g: 117;
+	--label-b: 202;
+	--label-h: 205;
+	--label-s: 100;
+	--label-l: 39;
+}
+
+[rgh-conventional-commits='build'] {
+	--label-r: 217;
+	--label-g: 63;
+	--label-b: 11;
+	--label-h: 15;
+	--label-s: 90;
+	--label-l: 44;
+}
+
+[rgh-conventional-commits='refactor'] {
+	--label-r: 0;
+	--label-g: 107;
+	--label-b: 117;
+	--label-h: 185;
+	--label-s: 100;
+	--label-l: 22;
+}
+
+[rgh-conventional-commits='test'] {
+	--label-r: 251;
+	--label-g: 202;
+	--label-b: 4;
+	--label-h: 48;
+	--label-s: 96;
+	--label-l: 50;
+}
+
+[rgh-conventional-commits='ci'] {
+	--label-r: 197;
+	--label-g: 222;
+	--label-b: 245;
+	--label-h: 208;
+	--label-s: 70;
+	--label-l: 86;
+}
+
+[rgh-conventional-commits='perf'] {
+	--label-r: 83;
+	--label-g: 25;
+	--label-b: 231;
+	--label-h: 256;
+	--label-s: 81;
+	--label-l: 50;
+}
diff --git a/source/features/conventional-commits.tsx b/source/features/conventional-commits.tsx
new file mode 100644
index 00000000..382d406c
--- /dev/null
+++ b/source/features/conventional-commits.tsx
@@ -0,0 +1,49 @@
+import './conventional-commits.css';
+import React from 'react';
+import * as pageDetect from 'github-url-detection';
+
+import features from '../feature-manager.js';
+import observe from '../helpers/selector-observer.js';
+import {commitTitleInLists} from '../github-helpers/selectors.js';
+import {parseConventionalCommit, removeCommitAndScope} from '../helpers/conventional-commits.js';
+
+function renderLabelInCommitTitle(commitTitleElement: HTMLElement): void {
+	const textNode = commitTitleElement.firstChild!;
+	const commit = parseConventionalCommit(textNode.textContent);
+
+	if (!commit) {
+		return;
+	}
+
+	commitTitleElement.prepend(
+		<span className="IssueLabel hx_IssueLabel mr-2" rgh-conventional-commits={commit.rawType}>
+			{commit.type}
+		</span>,
+		// Keep scope outside because that's how they're rendered in release notes as well
+		commit.scope ? <span style={{opacity: 0.7}}>{commit.scope}</span> : '',
+	);
+
+	removeCommitAndScope(textNode);
+}
+
+function init(signal: AbortSignal): void {
+	observe(`:is(${commitTitleInLists}) h4 > span > a:first-child`, renderLabelInCommitTitle, {signal});
+}
+
+void features.add(import.meta.url, {
+	include: [
+		pageDetect.isCommitList,
+	],
+	init,
+});
+
+/*
+
+Test URLs:
+
+- Repo commits: https://github.com/refined-github/sandbox/commits/conventional-commits/
+- PR commits: https://github.com/refined-github/sandbox/pull/91/commits
+- Real data: https://github.com/conventional-changelog/standard-version/commits
+- Repo without conventional commits: https://github.com/refined-github/refined-github/commits
+
+*/
diff --git a/source/features/cross-deleted-pr-branches.tsx b/source/features/cross-deleted-pr-branches.tsx
index 90d2d83e..386ceedf 100644
--- a/source/features/cross-deleted-pr-branches.tsx
+++ b/source/features/cross-deleted-pr-branches.tsx
@@ -21,7 +21,7 @@ function init(): void | false {
 	const deletedBranchName = lastBranchAction.textContent.trim();
 	const repoRootUrl = headReferenceLink?.href.split('/', 5).join('/');
 	for (const element of $$('.commit-ref')) {
-		const branchName = element.textContent.trim();
+		const branchName = element.textContent.trim().split(':').pop()!;
 		if (branchName === deletedBranchName) {
 			element.title = 'This branch has been deleted';
 
diff --git a/source/features/default-branch-button.css b/source/features/default-branch-button.css
new file mode 100644
index 00000000..2fa2a1c4
--- /dev/null
+++ b/source/features/default-branch-button.css
@@ -0,0 +1,9 @@
+.rgh-highlight-non-default-branch {
+	background-color: var(--bgColor-accent-muted, fuchsia) !important;
+	color: var(--fgColor-accent, fuchsia) !important;
+	border-color: var(--fgColor-accent, fuchsia) !important;
+}
+
+.rgh-highlight-non-default-branch svg {
+	color: var(--fgColor-accent, fuchsia) !important;
+}
diff --git a/source/features/default-branch-button.tsx b/source/features/default-branch-button.tsx
index 94eec67b..d664df1c 100644
--- a/source/features/default-branch-button.tsx
+++ b/source/features/default-branch-button.tsx
@@ -1,3 +1,4 @@
+import './default-branch-button.css';
 import React from 'dom-chef';
 import * as pageDetect from 'github-url-detection';
 import ChevronLeftIcon from 'octicons-plain-react/ChevronLeft';
@@ -39,6 +40,7 @@ async function add(branchSelector: HTMLElement): Promise<void> {
 	const selectorWrapper = branchSelector.tagName === 'SUMMARY'
 		? branchSelector.parentElement!
 		: branchSelector;
+	selectorWrapper.classList.add('rgh-highlight-non-default-branch');
 
 	const existingLink = $('.rgh-default-branch-button', branchSelector.parentElement!);
 
diff --git a/source/features/pr-branch-auto-delete.tsx b/source/features/pr-branch-auto-delete.tsx
index c88ef364..ce134696 100644
--- a/source/features/pr-branch-auto-delete.tsx
+++ b/source/features/pr-branch-auto-delete.tsx
@@ -7,31 +7,50 @@ import * as pageDetect from 'github-url-detection';
 import features from '../feature-manager.js';
 import onPrMerge from '../github-events/on-pr-merge.js';
 import featureLink from '../helpers/feature-link.js';
-import attachElement from '../helpers/attach-element.js';
 import {canEditEveryComment} from './quick-comment-edit.js';
+import {getBranches} from '../github-helpers/pr-branches.js';
+import matchesAnyPattern from '../helpers/matches-any-patterns.js';
 
 // TODO: Not an exact match; Moderators can edit comments but not create releases
 const canCreateRelease = canEditEveryComment;
 
+const exceptions = [
+	'dev',
+	'develop',
+	'development',
+	'main',
+	'master',
+	'next',
+	'pre',
+	'prod',
+	'stage',
+	/production/,
+	/^release\//,
+	/^v\d/,
+];
+
 async function init(): Promise<void> {
 	const deleteButton = $('[action$="/cleanup"] [type="submit"]');
 	if (!deleteButton) {
 		return;
 	}
 
+	if (matchesAnyPattern(getBranches().head.branch, exceptions)) {
+		return;
+	}
+
 	deleteButton.dataset.disableWith = 'Auto-deleting…';
 	deleteButton.click();
 
-	const deletionEvent = await elementReady('[data-test-selector="head-ref-deleted-event-ref-name"]', {
+	const deletionEvent = await elementReady('.TimelineItem-body:has(.pull-request-ref-restore-text)', {
 		stopOnDomReady: false,
+		timeout: 2000,
 	});
 
-	attachElement(deletionEvent!.closest('.TimelineItem-body'), {
-		append() {
-			const url = featureLink(features.getFeatureID(import.meta.url));
-			return <a className="d-inline-block" href={url}>via Refined GitHub <InfoIcon /></a>;
-		},
-	});
+	const url = featureLink(features.getFeatureID(import.meta.url));
+	deletionEvent!.append(
+		<a className="d-inline-block" href={url}>via Refined GitHub <InfoIcon /></a>,
+	);
 }
 
 void features.add(import.meta.url, {
diff --git a/source/helpers/bisect.tsx b/source/helpers/bisect.tsx
index f75f9ad5..24750519 100644
--- a/source/helpers/bisect.tsx
+++ b/source/helpers/bisect.tsx
@@ -35,7 +35,7 @@ async function onChoiceButtonClick({currentTarget: button}: React.MouseEvent<HTM
 	if (answer === 'yes') {
 		createMessageBox(
 			<>
-				<p>Unable to identify feature. It might be a <a href="https://github.com/refined-github/refined-github/wiki/Undo-unwanted-styles" target="_blank" rel="noreferrer">CSS-only feature</a>, a <a href="https://github.com/refined-github/refined-github/wiki/Meta-features" target="_blank" rel="noreferrer">meta-feature</a>, or unrelated to Refined GitHub.</p>
+				<p>Unable to identify feature. It might be a CSS-only feature, a <a href="https://github.com/refined-github/refined-github/wiki/Meta-features" target="_blank" rel="noreferrer">meta-feature</a>, or unrelated to Refined GitHub.</p>
 				<p>Try disabling Refined GitHub to see if the change or issue is caused by the extension.</p>
 			</>,
 		);
diff --git a/source/helpers/conventional-commits.ts b/source/helpers/conventional-commits.ts
new file mode 100644
index 00000000..d2375563
--- /dev/null
+++ b/source/helpers/conventional-commits.ts
@@ -0,0 +1,50 @@
+import {assertNodeContent} from './dom-utils.js';
+
+// Using https://www.conventionalcommits.org/ as a reference.
+const conventionalCommitRegex = /^(?<type>\w+)(?:\((?<scope>.+)\))?(?<major>!)?: /;
+
+const types = new Map([
+	['feat', 'Feature'],
+	['fix', 'Fix'],
+	['chore', 'Chore'],
+	['docs', 'Docs'],
+	['build', 'Build'],
+	['refactor', 'Refactor'],
+	['test', 'Test'],
+	['ci', 'CI'],
+	['perf', 'Performance'],
+]);
+
+export function parseConventionalCommit(commitTitle: string): {
+	rawType: string;
+	type: string;
+	scope?: string;
+	raw: string;
+} | undefined {
+	const match = conventionalCommitRegex.exec(commitTitle);
+	if (!match?.groups?.type) {
+		return;
+	}
+
+	const {type: rawType, scope, major} = match.groups;
+	const type = types.get(rawType);
+	if (!type) {
+		return;
+	}
+
+	return {
+		rawType,
+		type: major ? `${type}!` : type,
+		scope: scope ? `${scope}: ` : undefined,
+		raw: match[0],
+	};
+}
+
+/**
+Remove the raw commit prefix from the single text node.
+Note: It does not support pre-formatted titles like: fix(#1 `x`)
+*/
+export function removeCommitAndScope(textNode: Text | ChildNode): void {
+	assertNodeContent(textNode, conventionalCommitRegex);
+	textNode.textContent = textNode.textContent.replace(conventionalCommitRegex, '');
+}
diff --git a/source/helpers/dom-utils.ts b/source/helpers/dom-utils.ts
index 19f11ff3..99c6f395 100644
--- a/source/helpers/dom-utils.ts
+++ b/source/helpers/dom-utils.ts
@@ -7,7 +7,7 @@ import type {Nullable} from 'vitest';
 // This will set the correct `origin` header without having to use XMLHttpRequest
 // https://stackoverflow.com/questions/47356375/firefox-fetch-api-how-to-omit-the-origin-header-in-the-request
 // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#XHR_and_Fetch
-if (window.content?.fetch) {
+if (globalThis.window?.content?.fetch) {
 	setFetch(window.content.fetch);
 }
 
diff --git a/source/helpers/matches-any-patterns.ts b/source/helpers/matches-any-patterns.ts
new file mode 100644
index 00000000..e9ef230d
--- /dev/null
+++ b/source/helpers/matches-any-patterns.ts
@@ -0,0 +1,17 @@
+/** Tests a target string against a list of strings (full match) or regexes (can be mixed) */
+export default function matchesAnyPattern(
+	target: string,
+	patterns: Array<string | RegExp | ((x: string) => boolean)>,
+): boolean {
+	return patterns.some(pattern => {
+		if (typeof pattern === 'string') {
+			return pattern === target;
+		}
+
+		if (typeof pattern === 'function') {
+			return pattern(target);
+		}
+
+		return pattern.test(target);
+	});
+}
diff --git a/source/refined-github.ts b/source/refined-github.ts
index b156d884..7cbf6334 100644
--- a/source/refined-github.ts
+++ b/source/refined-github.ts
@@ -104,6 +104,7 @@ import './features/suggest-commit-title-limit.js';
 import './features/highest-rated-comment.js';
 import './features/clean-conversation-filters.js';
 import './features/tags-on-commits-list.js';
+import './features/conventional-commits.js';
 import './features/list-prs-for-file.js';
 import './features/pr-branch-auto-delete.js';
 import './features/linkify-symbolic-links.js'; // Must be before show-whitespace


Test patch:
diff --git a/build/features.test.ts b/build/features.test.ts
index ac98f535..2e6de30f 100755
--- a/build/features.test.ts
+++ b/build/features.test.ts
@@ -49,7 +49,9 @@ const imageRegex = /\.(png|gif)$/;
 
 const rghUploadsRegex = /refined-github[/]refined-github[/]assets[/]/;
 
-const screenshotRegex = regexJoinWithSeparator('|', [imageRegex, rghUploadsRegex]);
+const userAttachmentsRegex = /user-attachments[/]assets[/]/;
+
+const screenshotRegex = regexJoinWithSeparator('|', [imageRegex, rghUploadsRegex, userAttachmentsRegex]);
 
 class FeatureFile {
 	readonly id: FeatureID;
diff --git a/source/helpers/conventional-commits.test.ts b/source/helpers/conventional-commits.test.ts
new file mode 100644
index 00000000..50241d6a
--- /dev/null
+++ b/source/helpers/conventional-commits.test.ts
@@ -0,0 +1,68 @@
+import {test, expect} from 'vitest';
+
+import {parseConventionalCommit} from './conventional-commits.js';
+
+test('parseConventionalCommit', () => {
+	expect(parseConventionalCommit('fix: Commit message')).toMatchInlineSnapshot(`
+		{
+		  "raw": "fix: ",
+		  "rawType": "fix",
+		  "scope": undefined,
+		  "type": "Fix",
+		}
+	`);
+	expect(parseConventionalCommit('feat: Commit message')).toMatchInlineSnapshot(`
+		{
+		  "raw": "feat: ",
+		  "rawType": "feat",
+		  "scope": undefined,
+		  "type": "Feature",
+		}
+	`);
+	expect(parseConventionalCommit('fix!: Breaking change')).toMatchInlineSnapshot(`
+		{
+		  "raw": "fix!: ",
+		  "rawType": "fix",
+		  "scope": undefined,
+		  "type": "Fix!",
+		}
+	`);
+	expect(parseConventionalCommit('feat(scope): Commit message')).toMatchInlineSnapshot(`
+		{
+		  "raw": "feat(scope): ",
+		  "rawType": "feat",
+		  "scope": "scope: ",
+		  "type": "Feature",
+		}
+	`);
+	expect(parseConventionalCommit('feat(scope)!: Breaking change')).toMatchInlineSnapshot(`
+		{
+		  "raw": "feat(scope)!: ",
+		  "rawType": "feat",
+		  "scope": "scope: ",
+		  "type": "Feature!",
+		}
+	`);
+	expect(parseConventionalCommit('feat(sco pe): Commit message')).toMatchInlineSnapshot(`
+		{
+		  "raw": "feat(sco pe): ",
+		  "rawType": "feat",
+		  "scope": "sco pe: ",
+		  "type": "Feature",
+		}
+	`);
+	expect(parseConventionalCommit(('feat: Commit (message)'))).toMatchInlineSnapshot(`
+		{
+		  "raw": "feat: ",
+		  "rawType": "feat",
+		  "scope": undefined,
+		  "type": "Feature",
+		}
+	`);
+
+	expect(parseConventionalCommit('feat:')).toBeUndefined();
+	expect(parseConventionalCommit('idk(label): not recognized')).toBeUndefined();
+	expect(parseConventionalCommit('Commit message')).toBeUndefined();
+	expect(parseConventionalCommit('feat(): Commit message')).toBeUndefined();
+	expect(parseConventionalCommit('fe at(scope): Commit message) ')).toBeUndefined();
+});