您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.)
当前为
// ==UserScript== // @name GitHub Commit Labels // @namespace https://github.com/nazdridoy // @version 1.0.1 // @description Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.) // @author nazdridoy // @license MIT // @match https://github.com/* // @icon https://github.githubassets.com/favicons/favicon.svg // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-end // @homepageURL https://github.com/nazdridoy/github-commit-labels // @supportURL https://github.com/nazdridoy/github-commit-labels/issues // ==/UserScript== /* MIT License Copyright (c) 2025 nazDridoy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function() { 'use strict'; // Color definitions using HSL values const COLORS = { 'green': { bg: 'rgba(35, 134, 54, 0.2)', text: '#7ee787' }, 'purple': { bg: 'rgba(163, 113, 247, 0.2)', text: '#d2a8ff' }, 'blue': { bg: 'rgba(47, 129, 247, 0.2)', text: '#79c0ff' }, 'light-blue': { bg: 'rgba(31, 111, 235, 0.2)', text: '#58a6ff' }, 'yellow': { bg: 'rgba(210, 153, 34, 0.2)', text: '#e3b341' }, 'orange': { bg: 'rgba(219, 109, 40, 0.2)', text: '#ffa657' }, 'gray': { bg: 'rgba(139, 148, 158, 0.2)', text: '#8b949e' }, 'light-green': { bg: 'rgba(57, 211, 83, 0.2)', text: '#56d364' }, 'red': { bg: 'rgba(248, 81, 73, 0.2)', text: '#ff7b72' }, 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.2)', text: '#bb8009' } }; // Define default configuration const DEFAULT_CONFIG = { removePrefix: true, labelStyle: { fontSize: '14px', fontWeight: '500', height: '24px', padding: '0 10px', marginRight: '8px', borderRadius: '20px', minWidth: 'auto', textAlign: 'center', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'nowrap', background: 'rgba(0, 0, 0, 0.2)', backdropFilter: 'blur(4px)', border: '1px solid rgba(240, 246, 252, 0.1)', // Subtle border color: '#ffffff' }, commitTypes: { // Features feat: { emoji: '✨', label: 'Feature', color: 'green' }, feature: { emoji: '✨', label: 'Feature', color: 'green' }, // Added added: { emoji: '📝', label: 'Added', color: 'green' }, add: { emoji: '📝', label: 'Added', color: 'green' }, // Updated update: { emoji: '♻️', label: 'Updated', color: 'blue' }, updated: { emoji: '♻️', label: 'Updated', color: 'blue' }, // Removed removed: { emoji: '🗑️', label: 'Removed', color: 'red' }, remove: { emoji: '🗑️', label: 'Removed', color: 'red' }, // Fixes fix: { emoji: '🐛', label: 'Fix', color: 'purple' }, bugfix: { emoji: '🐛', label: 'Fix', color: 'purple' }, fixed: { emoji: '🐛', label: 'Fix', color: 'purple' }, hotfix: { emoji: '🚨', label: 'Hot Fix', color: 'red' }, // Documentation docs: { emoji: '📚', label: 'Docs', color: 'blue' }, doc: { emoji: '📚', label: 'Docs', color: 'blue' }, documentation: { emoji: '📚', label: 'Docs', color: 'blue' }, // Styling style: { emoji: '💎', label: 'Style', color: 'light-green' }, ui: { emoji: '🎨', label: 'UI', color: 'light-green' }, css: { emoji: '💎', label: 'Style', color: 'light-green' }, // Code Changes refactor: { emoji: '📦', label: 'Refactor', color: 'light-blue' }, perf: { emoji: '🚀', label: 'Performance', color: 'purple' }, performance: { emoji: '🚀', label: 'Performance', color: 'purple' }, optimize: { emoji: '⚡', label: 'Optimize', color: 'purple' }, // Testing test: { emoji: '🧪', label: 'Test', color: 'yellow' }, tests: { emoji: '🧪', label: 'Test', color: 'yellow' }, testing: { emoji: '🧪', label: 'Test', color: 'yellow' }, // Build & Deploy build: { emoji: '🛠', label: 'Build', color: 'orange' }, ci: { emoji: '⚙️', label: 'CI', color: 'gray' }, cd: { emoji: '🚀', label: 'CD', color: 'gray' }, deploy: { emoji: '📦', label: 'Deploy', color: 'orange' }, release: { emoji: '🚀', label: 'Deploy', color: 'orange' }, // Maintenance chore: { emoji: '♻️', label: 'Chore', color: 'light-green' }, deps: { emoji: '📦', label: 'Dependencies', color: 'light-green' }, dep: { emoji: '📦', label: 'Dependencies', color: 'light-green' }, dependencies: { emoji: '📦', label: 'Dependencies', color: 'light-green' }, revert: { emoji: '🗑', label: 'Revert', color: 'red' }, wip: { emoji: '🚧', label: 'WIP', color: 'dark-yellow' } } }; // Get saved configuration or use default const USER_CONFIG = GM_getValue('commitLabelsConfig', DEFAULT_CONFIG); // Create configuration window function createConfigWindow() { const configWindow = document.createElement('div'); configWindow.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 20px; z-index: 9999; width: 600px; max-height: 80vh; overflow-y: auto; color: #c9d1d9; box-shadow: 0 0 10px rgba(0,0,0,0.5); `; const title = document.createElement('h2'); title.textContent = 'Commit Labels Configuration'; title.style.marginBottom = '20px'; configWindow.appendChild(title); // Remove Prefix Option const prefixDiv = document.createElement('div'); prefixDiv.style.marginBottom = '20px'; const prefixCheckbox = document.createElement('input'); prefixCheckbox.type = 'checkbox'; prefixCheckbox.checked = USER_CONFIG.removePrefix; prefixCheckbox.id = 'remove-prefix'; const prefixLabel = document.createElement('label'); prefixLabel.htmlFor = 'remove-prefix'; prefixLabel.textContent = ' Remove commit type prefix from message'; prefixDiv.appendChild(prefixCheckbox); prefixDiv.appendChild(prefixLabel); configWindow.appendChild(prefixDiv); // Commit Types Configuration const typesContainer = document.createElement('div'); typesContainer.style.marginBottom = '20px'; // Group commit types by their label const groupedTypes = {}; Object.entries(USER_CONFIG.commitTypes).forEach(([type, config]) => { const key = config.label; if (!groupedTypes[key]) { groupedTypes[key] = { types: [], config: config }; } groupedTypes[key].types.push(type); }); // Create rows for grouped types Object.entries(groupedTypes).forEach(([label, { types, config }]) => { const typeDiv = document.createElement('div'); typeDiv.style.marginBottom = '10px'; typeDiv.style.display = 'flex'; typeDiv.style.alignItems = 'center'; typeDiv.style.gap = '10px'; // Type names (with aliases) and edit button container const typeContainer = document.createElement('div'); typeContainer.style.display = 'flex'; typeContainer.style.width = '150px'; typeContainer.style.alignItems = 'center'; typeContainer.style.gap = '4px'; const typeSpan = document.createElement('span'); typeSpan.style.color = '#8b949e'; typeSpan.style.flex = '1'; typeSpan.textContent = types.join(', ') + ':'; const editAliasButton = document.createElement('button'); editAliasButton.textContent = '✏️'; editAliasButton.title = 'Edit Aliases'; editAliasButton.style.cssText = ` padding: 2px 4px; background: #21262d; color: #58a6ff; border: 1px solid #30363d; border-radius: 4px; cursor: pointer; font-size: 10px; `; editAliasButton.onclick = () => { const currentAliases = types.join(', '); const newAliases = prompt('Edit aliases (separate with commas):', currentAliases); if (newAliases && newAliases.trim()) { const newTypes = newAliases.split(',').map(t => t.trim().toLowerCase()).filter(t => t); // Check if any new aliases conflict with other types const conflictingType = newTypes.find(type => USER_CONFIG.commitTypes[type] && !types.includes(type) ); if (conflictingType) { alert(`The alias "${conflictingType}" already exists in another group!`); return; } // Remove old types types.forEach(type => delete USER_CONFIG.commitTypes[type]); // Add new types with same config newTypes.forEach(type => { USER_CONFIG.commitTypes[type] = { ...config }; }); // Update the display typeSpan.textContent = newTypes.join(', ') + ':'; // Update dataset for inputs const inputs = typeDiv.querySelectorAll('input, select'); inputs.forEach(input => { input.dataset.types = newTypes.join(','); }); } }; typeContainer.appendChild(typeSpan); typeContainer.appendChild(editAliasButton); typeDiv.appendChild(typeContainer); // Emoji input const emojiInput = document.createElement('input'); emojiInput.type = 'text'; emojiInput.value = config.emoji; emojiInput.style.width = '40px'; emojiInput.dataset.types = types.join(','); emojiInput.dataset.field = 'emoji'; typeDiv.appendChild(emojiInput); // Label input const labelInput = document.createElement('input'); labelInput.type = 'text'; labelInput.value = config.label; labelInput.style.width = '120px'; labelInput.dataset.types = types.join(','); labelInput.dataset.field = 'label'; typeDiv.appendChild(labelInput); // Color select const colorSelect = document.createElement('select'); Object.keys(COLORS).forEach(color => { const option = document.createElement('option'); option.value = color; option.textContent = color; if (config.color === color) option.selected = true; colorSelect.appendChild(option); }); colorSelect.dataset.types = types.join(','); colorSelect.dataset.field = 'color'; typeDiv.appendChild(colorSelect); // Delete button const deleteButton = document.createElement('button'); deleteButton.textContent = '🗑️'; deleteButton.style.cssText = ` padding: 2px 8px; background: #21262d; color: #f85149; border: 1px solid #30363d; border-radius: 4px; cursor: pointer; `; deleteButton.onclick = () => { if (confirm(`Delete commit types "${types.join(', ')}"?`)) { typeDiv.remove(); types.forEach(type => delete USER_CONFIG.commitTypes[type]); } }; typeDiv.appendChild(deleteButton); typesContainer.appendChild(typeDiv); }); // Add "Add New Type" button const addNewButton = document.createElement('button'); addNewButton.textContent = '+ Add New Type'; addNewButton.style.cssText = ` margin-bottom: 15px; padding: 5px 16px; background: #238636; color: #fff; border: none; border-radius: 6px; cursor: pointer; `; addNewButton.onclick = () => { const typeInput = prompt('Enter the commit type and aliases (separated by commas, e.g., "added, add"):', ''); if (typeInput && typeInput.trim()) { const types = typeInput.split(',').map(t => t.trim().toLowerCase()).filter(t => t); // Check if any of the types already exist const existingType = types.find(type => USER_CONFIG.commitTypes[type]); if (existingType) { alert(`The commit type "${existingType}" already exists!`); return; } // Create base config for all aliases const baseConfig = { emoji: '🔄', label: types[0].charAt(0).toUpperCase() + types[0].slice(1), color: 'blue' }; // Add all types to config with the same settings types.forEach(type => { USER_CONFIG.commitTypes[type] = { ...baseConfig }; }); // Create and add new type row const typeDiv = document.createElement('div'); typeDiv.style.marginBottom = '10px'; typeDiv.style.display = 'flex'; typeDiv.style.alignItems = 'center'; typeDiv.style.gap = '10px'; // Type names (with aliases) const typeSpan = document.createElement('span'); typeSpan.style.width = '150px'; typeSpan.style.color = '#8b949e'; typeSpan.textContent = types.join(', ') + ':'; typeDiv.appendChild(typeSpan); // Emoji input const emojiInput = document.createElement('input'); emojiInput.type = 'text'; emojiInput.value = baseConfig.emoji; emojiInput.style.width = '40px'; emojiInput.dataset.types = types.join(','); emojiInput.dataset.field = 'emoji'; typeDiv.appendChild(emojiInput); // Label input const labelInput = document.createElement('input'); labelInput.type = 'text'; labelInput.value = baseConfig.label; labelInput.style.width = '120px'; labelInput.dataset.types = types.join(','); labelInput.dataset.field = 'label'; typeDiv.appendChild(labelInput); // Color select const colorSelect = document.createElement('select'); Object.keys(COLORS).forEach(color => { const option = document.createElement('option'); option.value = color; option.textContent = color; if (color === 'blue') option.selected = true; colorSelect.appendChild(option); }); colorSelect.dataset.types = types.join(','); colorSelect.dataset.field = 'color'; typeDiv.appendChild(colorSelect); // Delete button const deleteButton = document.createElement('button'); deleteButton.textContent = '🗑️'; deleteButton.style.cssText = ` padding: 2px 8px; background: #21262d; color: #f85149; border: 1px solid #30363d; border-radius: 4px; cursor: pointer; `; deleteButton.onclick = () => { if (confirm(`Delete commit types "${types.join(', ')}"?`)) { typeDiv.remove(); types.forEach(type => delete USER_CONFIG.commitTypes[type]); } }; typeDiv.appendChild(deleteButton); typesContainer.appendChild(typeDiv); } }; configWindow.appendChild(addNewButton); configWindow.appendChild(typesContainer); // Save and Close buttons const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; buttonContainer.style.justifyContent = 'flex-end'; const saveButton = document.createElement('button'); saveButton.textContent = 'Save'; saveButton.style.cssText = ` padding: 5px 16px; background: #238636; color: #fff; border: none; border-radius: 6px; cursor: pointer; `; const closeButton = document.createElement('button'); closeButton.textContent = 'Close'; closeButton.style.cssText = ` padding: 5px 16px; background: #21262d; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; cursor: pointer; `; // Add Reset button next to Save and Close const resetButton = document.createElement('button'); resetButton.textContent = 'Reset to Default'; resetButton.style.cssText = ` padding: 5px 16px; background: #21262d; color: #f85149; border: 1px solid #30363d; border-radius: 6px; cursor: pointer; margin-right: auto; // This pushes Save/Close to the right `; resetButton.onclick = () => { if (confirm('Are you sure you want to reset all settings to default? This will remove all custom types and settings.')) { GM_setValue('commitLabelsConfig', DEFAULT_CONFIG); location.reload(); } }; saveButton.onclick = () => { const newConfig = { ...USER_CONFIG }; newConfig.removePrefix = prefixCheckbox.checked; newConfig.commitTypes = {}; typesContainer.querySelectorAll('input, select').forEach(input => { const types = input.dataset.types.split(','); const field = input.dataset.field; types.forEach(type => { if (!newConfig.commitTypes[type]) { newConfig.commitTypes[type] = {}; } newConfig.commitTypes[type][field] = input.value; }); }); GM_setValue('commitLabelsConfig', newConfig); location.reload(); }; closeButton.onclick = () => { document.body.removeChild(configWindow); }; buttonContainer.appendChild(resetButton); buttonContainer.appendChild(closeButton); buttonContainer.appendChild(saveButton); configWindow.appendChild(buttonContainer); document.body.appendChild(configWindow); } // Register configuration menu command GM_registerMenuCommand('Configure Commit Labels', createConfigWindow); // Check if we're on a commit page function isCommitPage() { return window.location.pathname.includes('/commits') || window.location.pathname.includes('/commit/'); } function addCommitLabels() { // Only proceed if we're on a commit page if (!isCommitPage()) return; // Update selector to match GitHub's current DOM structure const commitMessages = document.querySelectorAll('.markdown-title a[data-pjax="true"]'); commitMessages.forEach(message => { const text = message.textContent.trim(); const match = text.match(/^(\w+)(?:\([\w-]+\))?:\s*(.*)/); if (match) { const type = match[1].toLowerCase(); const restOfMessage = match[2]; if (USER_CONFIG.commitTypes[type]) { // Only add label if it hasn't been added yet if (!message.parentElement.querySelector('.commit-label')) { const label = document.createElement('span'); label.className = 'commit-label'; const color = COLORS[USER_CONFIG.commitTypes[type].color]; // Calculate perceived lightness (using GitHub's formula) const perceivedL = (color.l / 100); const lightnessSwitch = perceivedL <= 0.6 ? 1 : 0; const lightenBy = ((0.6 - perceivedL) * 100) * lightnessSwitch; // Apply styles const styles = { ...USER_CONFIG.labelStyle, '--label-h': color.h, '--label-s': color.s, '--label-l': color.l, 'color': `hsl(${color.h}, ${color.s}%, ${color.l + lightenBy}%)`, 'background': `hsla(${color.h}, ${color.s}%, ${color.l}%, 0.18)`, 'borderColor': `hsla(${color.h}, ${color.s}%, ${color.l + lightenBy}%, 0.3)`, backgroundColor: color.bg, color: color.text }; label.style.cssText = Object.entries(styles) .map(([key, value]) => `${key.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${value}`) .join(';'); // Add hover effect label.addEventListener('mouseenter', () => { label.style.transform = 'translateY(-1px)'; label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)'; }); label.addEventListener('mouseleave', () => { label.style.transform = 'translateY(0)'; label.style.boxShadow = styles.boxShadow; }); const emoji = document.createElement('span'); emoji.style.marginRight = '4px'; emoji.style.fontSize = '14px'; emoji.style.lineHeight = '1'; emoji.textContent = USER_CONFIG.commitTypes[type].emoji; const labelText = document.createElement('span'); labelText.textContent = USER_CONFIG.commitTypes[type].label; label.appendChild(emoji); label.appendChild(labelText); message.parentElement.insertBefore(label, message); // Update the commit message text to remove the type prefix if enabled if (USER_CONFIG.removePrefix) { message.textContent = restOfMessage; } } } } }); } // Only set up observers if we're on a commit page function initialize() { // Initial run addCommitLabels(); // Watch for DOM changes const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { addCommitLabels(); } } }); // Start observing the document with the configured parameters observer.observe(document.body, { childList: true, subtree: true }); } // Initialize on page load initialize(); // Handle GitHub's client-side navigation const navigationObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { // Check if we're on a commit page after navigation if (isCommitPage()) { // Small delay to ensure GitHub has finished rendering setTimeout(addCommitLabels, 100); } } } }); // Observe changes to the main content area navigationObserver.observe(document.body, { childList: true, subtree: true }); // Listen for popstate events (browser back/forward navigation) window.addEventListener('popstate', () => { if (isCommitPage()) { setTimeout(addCommitLabels, 100); } }); // Listen for GitHub's custom navigation event document.addEventListener('turbo:render', () => { if (isCommitPage()) { setTimeout(addCommitLabels, 100); } }); })();