Repository: refined-github/refined-github
Pull request #7794 (2024-09-08 08:51:58)

`cross-deleted-pr-branches` - Restore feature
Closes: #7708 


## Test URLs

- deleted branch: https://github.com/refined-github/refined-github/pull/576
- deleted branch (from fork): https://github.com/refined-github/refined-github/pull/872
- restored branch (on fork): https://github.com/refined-github/refined-github/pull/909
- deleted repository: https://github.com/refined-github/refined-github/pull/271
- deleted repository on open PR: https://github.com/yakov116/TestR/pull/7
- deleted repository with no deleted branch https://github.com/jquery/jquery/pull/769

## Screenshot

https://github.com/user-attachments/assets/841eb449-921c-461d-a777-0ddce12ea689



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/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..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/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/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/.github/workflows/test.yml b/.github/workflows/test.yml
index 68947be4..0f73d32b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,6 +24,7 @@ jobs:
         with:
           node-version-file: package.json
           cache: npm
+      - run: sudo apt-get install ripgrep
       - run: npm ci
       - run: npm run vitest -- --exclude 'build/features*' --exclude '**/selectors*'
       - run: npm run vitest -- selectors
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) '));
+});