Repository: refined-github/refined-github (ts)

Issue:
Display conventional commits as colored labels before the commit message 
## Description

Enhance the display of commit messages by rendering SemVer tags as colored badges, improving clarity and readability.

## Visual Example

Current commit view:

![Current View](https://github.com/user-attachments/assets/28e17e8c-dda6-4617-bde2-69d7ba91372d)

Proposed improvement:

![Proposed view](https://github.com/user-attachments/assets/3aca12c2-9416-402b-b9bb-123326e5cc96)

## Detailed Explanation

`semantic-release` prefixes commit messages with conventional labels (e.g., `feat`, `fix`, `chore`), creating a structured naming convention. Currently, commit titles look like this:

![Example Commit](https://github.com/user-attachments/assets/0406d948-a34c-4c0b-939e-171703d44214)

GitHub does not visually distinguish SemVer prefixes from the rest of the commit message, making it harder to quickly identify commit types. These prefixes, however, are crucial for tools that automatically generate changelogs and versioning.

By transforming these prefixes into colored badges, we can instantly communicate the purpose of each commit:
- **Red** for `fix`
- **Green** for `feat`
- **Gray** for `chore`
- **Orange/Brown** for `refactor` (and similar types)

The scope (inside parentheses) could be displayed next to the commit type, as shown in the example.

## Customization Options

To increase flexibility, the following options could be added:

- **Badge Position**: Display the badge either *before* or *after* the commit title.
- **Color Customization**: Allow users to customize badge colors for each commit type.
- **Custom Types**: Let users define additional commit types and assign specific colors to them.

## Guideline Compliance:

- **Existing solutions**: No known userscripts or extensions provide this functionality after a thorough search.
- **Universal applicability**: SemVer is widely adopted in libraries, making this feature useful for most users.
- **No options required**: The SemVer specification dictates clear rules, so additional options are unnecessary. Customization (e.g., color changes) could be optional.
- **Minimal UI/code effort**: This feature only replaces the prefix with a colored badge, requiring minimal code changes.
- **No external server**: The badge rendering can happen statically, with no need for server interactions.
- **No increased HTTP traffic**: The solution is entirely client-side and does not introduce additional network requests.
- **Scope of enhancement**: This enhancement targets commit views, not GitHub Projects.

### Example URLs

https://github.com/revanced/revanced-patches/commits (In the list of commits)
https://github.com/ReVanced/revanced-patches/commit/09e3b3044824d0317c408637d157d93d0201272c (Where the commit is shown)
https://github.com/revanced/revanced-patches (Where the last commit is shown)

Pull request:
`conventional-commits` - New feature
Closes #7795.

## Test URLs

https://github.com/ReVanced/revanced-patches/commits/main/

## Screenshot

https://github.com/user-attachments/assets/630f4be4-e1fb-4315-89a6-1cf8d87612f8

# Todo

This PR is still a draft and is awaiting many changes such as:

- [x] Display labels instantly (Help needed from maintainers)
- [x] Render everywhere where commits are shown `pageDetect.isRepoCommitList for some reason doesn't work` (Help needed from maintainers)
- [x] Render when clicking on "Next" or "Previous" in the commit list instead of refreshing (and similar actions) (Help needed from maintainers)
- [x] Implement missing default commit types
- ~~[ ] Add options to specify color, custom commit types , to hide the scope and display the label before or after the commit message~~

Patch:
diff --git a/build/__snapshots__/features-meta.json b/build/__snapshots__/features-meta.json
index 4b5b778e..26b895d2 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/a031ec07-c655-4f80-b43e-80ab8ece55ea"
+	},
 	{
 		"id": "conversation-activity-filter",
 		"description": "Lets you hide every event except comments or unresolved comments in issues and PRs.",
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..6358edab 100644
--- a/readme.md
+++ b/readme.md
@@ -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/a031ec07-c655-4f80-b43e-80ab8ece55ea)
 
 <!-- Refer to style guide above. Keep this message between sections. -->
 
diff --git a/source/features/conventional-commits.css b/source/features/conventional-commits.css
new file mode 100644
index 00000000..85f5b8d5
--- /dev/null
+++ b/source/features/conventional-commits.css
@@ -0,0 +1,85 @@
+.rgh-commit-type-label-feat-colors {
+	--label-r: 14;
+	--label-g: 138;
+	--label-b: 22;
+	--label-h: 123;
+	--label-s: 81;
+	--label-l: 29;
+}
+
+.rgh-commit-type-label-fix-colors {
+	--label-r: 182;
+	--label-g: 2;
+	--label-b: 5;
+	--label-h: 359;
+	--label-s: 97;
+	--label-l: 36;
+}
+
+.rgh-commit-type-label-chore-colors {
+	--label-r: 254;
+	--label-g: 242;
+	--label-b: 192;
+	--label-h: 48;
+	--label-s: 96;
+	--label-l: 87;
+}
+
+.rgh-commit-type-label-docs-colors {
+	--label-r: 249;
+	--label-g: 208;
+	--label-b: 196;
+	--label-h: 13;
+	--label-s: 81;
+	--label-l: 87;
+}
+
+.rgh-commit-type-label-build-colors {
+	--label-r: 217;
+	--label-g: 63;
+	--label-b: 11;
+	--label-h: 15;
+	--label-s: 90;
+	--label-l: 44;
+}
+
+.rgh-commit-type-label-refactor-colors {
+	--label-r: 0;
+	--label-g: 107;
+	--label-b: 117;
+	--label-h: 185;
+	--label-s: 100;
+	--label-l: 22;
+}
+
+.rgh-commit-type-label-test-colors {
+	--label-r: 251;
+	--label-g: 202;
+	--label-b: 4;
+	--label-h: 48;
+	--label-s: 96;
+	--label-l: 50;
+}
+
+.rgh-commit-type-label-ci-colors {
+	--label-r: 197;
+	--label-g: 222;
+	--label-b: 245;
+	--label-h: 208;
+	--label-s: 70;
+	--label-l: 86;
+}
+
+.rgh-commit-type-label-perf-colors {
+	--label-r: 83;
+	--label-g: 25;
+	--label-b: 231;
+	--label-h: 256;
+	--label-s: 81;
+	--label-l: 50;
+}
+
+.rgh-commit-type-label {
+	font-size: var(--body-font-size, 14px) !important;
+	padding: 2px 0.5rem !important;
+}
diff --git a/source/features/conventional-commits.tsx b/source/features/conventional-commits.tsx
new file mode 100644
index 00000000..1b91df07
--- /dev/null
+++ b/source/features/conventional-commits.tsx
@@ -0,0 +1,71 @@
+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';
+
+const types = new Map([
+	['feat', 'Feature'],
+	['fix', 'Fix'],
+	['chore', 'Chore'],
+	['docs', 'Docs'],
+	['build', 'Build'],
+	['refactor', 'Refactor'],
+	['test', 'Test'],
+	['ci', 'CI'],
+	['perf', 'Performance'],
+]);
+
+function createLabelElement(type: string, scope?: string): JSX.Element {
+	const label = types.get(type)!;
+
+	return (
+		<span className={`IssueLabel hx_IssueLabel rgh-commit-type-label rgh-commit-type-label-${type}-colors`}>
+			{scope ? `${label}: ${scope}` : label}
+		</span>
+	);
+}
+
+function renderLabelInCommitTitle(commitTitleElement: HTMLElement): void {
+	const match = parseConventionalCommit(commitTitleElement.textContent);
+
+	const {type, scope} = match?.groups ?? {};
+	if (!type || !types.has(type)) {
+		return;
+	}
+
+	commitTitleElement.prepend(createLabelElement(type, scope));
+	removeCommitAndScope(commitTitleElement, match!);
+}
+
+function initRepoCommitList(signal: AbortSignal): void {
+	observe(`:is(${commitTitleInLists}) h4 > span`, renderLabelInCommitTitle, {signal});
+}
+
+function initPrCommitList(signal: AbortSignal): void {
+	observe(`:is(${commitTitleInLists}) a`, renderLabelInCommitTitle, {signal});
+}
+
+void features.add(import.meta.url, {
+	include: [
+		pageDetect.isRepoCommitList,
+	],
+	init: initRepoCommitList,
+}, {
+	include: [
+		pageDetect.isPRCommitList,
+	],
+	init: initPrCommitList,
+});
+
+/*
+
+Test URLs:
+
+https://github.com/semantic-release/semantic-release/commits/master/
+https://github.com/ReVanced/revanced-patches/commits/main/
+
+*/
diff --git a/source/helpers/conventional-commits.ts b/source/helpers/conventional-commits.ts
new file mode 100644
index 00000000..490cd8a5
--- /dev/null
+++ b/source/helpers/conventional-commits.ts
@@ -0,0 +1,26 @@
+import zipTextNodes from 'zip-text-nodes';
+
+// Using https://www.conventionalcommits.org/ as a reference.
+const CONVENTIONAL_COMMIT_REGEX = /^(?<type>\w+)(?:\((?<scope>.+)\))?: /;
+
+export function parseConventionalCommit(commitTitle: string): RegExpExecArray | undefined {
+	return CONVENTIONAL_COMMIT_REGEX.exec(commitTitle) ?? undefined;
+}
+
+export function removeCommitAndScope(
+	element: HTMLElement,
+	match: RegExpExecArray,
+): void {
+	const {type, scope} = match?.groups ?? {};
+
+	const semanticCommitTypeAndScope = scope ? `${type}(${scope}):` : `${type}:`;
+
+	const modified = element.textContent.replace(semanticCommitTypeAndScope, match => `
${match}
`);
+
+	const temporaryDiv = document.createElement('div');
+	temporaryDiv.innerHTML = modified;
+
+	zipTextNodes(element, temporaryDiv);
+
+	element.querySelector('s')!.remove();
+}
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..a2707ef4
--- /dev/null
+++ b/source/helpers/conventional-commits.test.ts
@@ -0,0 +1,16 @@
+import {test, assert} from 'vitest';
+
+import {parseConventionalCommit} from './conventional-commits.js';
+
+test('parseConventionalCommit', () => {
+	assert.deepStrictEqual(parseConventionalCommit('fix: Commit message'), ['fix: ', 'fix', undefined]);
+	assert.deepStrictEqual(parseConventionalCommit('feat: Commit message'), ['feat: ', 'feat', undefined]);
+	assert.deepStrictEqual(parseConventionalCommit('feat(scope): Commit message'), ['feat(scope): ', 'feat', 'scope']);
+	assert.deepStrictEqual(parseConventionalCommit('feat(sco pe): Commit message'), ['feat(sco pe): ', 'feat', 'sco pe']);
+	assert.deepStrictEqual(parseConventionalCommit(('feat: Commit (message)')), ['feat: ', 'feat', undefined]);
+
+	assert.isUndefined(parseConventionalCommit('feat:'));
+	assert.isUndefined(parseConventionalCommit('Commit message'));
+	assert.isUndefined(parseConventionalCommit('feat(): Commit message'));
+	assert.isUndefined(parseConventionalCommit('fe at(scope): Commit message) '));
+});