GitHub Commit Labels

Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.)

目前为 2025-02-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Commit Labels
  3. // @namespace https://github.com/nazdridoy
  4. // @version 1.0.0
  5. // @description Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.)
  6. // @author nazdridoy
  7. // @license MIT
  8. // @match https://github.com/*
  9. // @icon https://github.githubassets.com/favicons/favicon.svg
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_registerMenuCommand
  13. // @run-at document-end
  14. // @homepageURL https://github.com/nazdridoy/github-commit-labels
  15. // @supportURL https://github.com/nazdridoy/github-commit-labels/issues
  16. // ==/UserScript==
  17.  
  18. /*
  19. MIT License
  20.  
  21. Copyright (c) 2025 nazDridoy
  22.  
  23. Permission is hereby granted, free of charge, to any person obtaining a copy
  24. of this software and associated documentation files (the "Software"), to deal
  25. in the Software without restriction, including without limitation the rights
  26. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  27. copies of the Software, and to permit persons to whom the Software is
  28. furnished to do so, subject to the following conditions:
  29.  
  30. The above copyright notice and this permission notice shall be included in all
  31. copies or substantial portions of the Software.
  32.  
  33. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  34. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  35. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  36. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  37. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  38. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  39. SOFTWARE.
  40. */
  41.  
  42. (function() {
  43. 'use strict';
  44.  
  45. // Color definitions using HSL values
  46. const COLORS = {
  47. 'green': { bg: 'rgba(35, 134, 54, 0.2)', text: '#7ee787' },
  48. 'purple': { bg: 'rgba(163, 113, 247, 0.2)', text: '#d2a8ff' },
  49. 'blue': { bg: 'rgba(47, 129, 247, 0.2)', text: '#79c0ff' },
  50. 'light-blue': { bg: 'rgba(31, 111, 235, 0.2)', text: '#58a6ff' },
  51. 'yellow': { bg: 'rgba(210, 153, 34, 0.2)', text: '#e3b341' },
  52. 'orange': { bg: 'rgba(219, 109, 40, 0.2)', text: '#ffa657' },
  53. 'gray': { bg: 'rgba(139, 148, 158, 0.2)', text: '#8b949e' },
  54. 'light-green': { bg: 'rgba(57, 211, 83, 0.2)', text: '#56d364' },
  55. 'red': { bg: 'rgba(248, 81, 73, 0.2)', text: '#ff7b72' },
  56. 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.2)', text: '#bb8009' }
  57. };
  58.  
  59. // Define default configuration
  60. const DEFAULT_CONFIG = {
  61. removePrefix: true,
  62. labelStyle: {
  63. fontSize: '14px',
  64. fontWeight: '500',
  65. height: '24px',
  66. padding: '0 10px',
  67. marginRight: '8px',
  68. borderRadius: '20px',
  69. minWidth: 'auto',
  70. textAlign: 'center',
  71. display: 'inline-flex',
  72. alignItems: 'center',
  73. justifyContent: 'center',
  74. whiteSpace: 'nowrap',
  75. background: 'rgba(0, 0, 0, 0.2)',
  76. backdropFilter: 'blur(4px)',
  77. border: '1px solid rgba(240, 246, 252, 0.1)', // Subtle border
  78. color: '#ffffff'
  79. },
  80. commitTypes: {
  81. // Features
  82. feat: { emoji: '✨', label: 'Feature', color: 'green' },
  83. feature: { emoji: '✨', label: 'Feature', color: 'green' },
  84.  
  85. // Added
  86. added: { emoji: '📝', label: 'Added', color: 'green' },
  87. add: { emoji: '📝', label: 'Added', color: 'green' },
  88.  
  89. // Updated
  90. update: { emoji: '♻️', label: 'Updated', color: 'blue' },
  91. updated: { emoji: '♻️', label: 'Updated', color: 'blue' },
  92.  
  93. // Removed
  94. removed: { emoji: '🗑️', label: 'Removed', color: 'red' },
  95. remove: { emoji: '🗑️', label: 'Removed', color: 'red' },
  96.  
  97. // Fixes
  98. fix: { emoji: '🐛', label: 'Fix', color: 'purple' },
  99. bugfix: { emoji: '🐛', label: 'Fix', color: 'purple' },
  100. fixed: { emoji: '🐛', label: 'Fix', color: 'purple' },
  101. hotfix: { emoji: '🚨', label: 'Hot Fix', color: 'red' },
  102.  
  103. // Documentation
  104. docs: { emoji: '📚', label: 'Docs', color: 'blue' },
  105. doc: { emoji: '📚', label: 'Docs', color: 'blue' },
  106. documentation: { emoji: '📚', label: 'Docs', color: 'blue' },
  107.  
  108. // Styling
  109. style: { emoji: '💎', label: 'Style', color: 'light-green' },
  110. ui: { emoji: '🎨', label: 'UI', color: 'light-green' },
  111. css: { emoji: '💎', label: 'Style', color: 'light-green' },
  112.  
  113. // Code Changes
  114. refactor: { emoji: '📦', label: 'Refactor', color: 'light-blue' },
  115. perf: { emoji: '🚀', label: 'Performance', color: 'purple' },
  116. performance: { emoji: '🚀', label: 'Performance', color: 'purple' },
  117. optimize: { emoji: '⚡', label: 'Optimize', color: 'purple' },
  118.  
  119. // Testing
  120. test: { emoji: '🧪', label: 'Test', color: 'yellow' },
  121. tests: { emoji: '🧪', label: 'Test', color: 'yellow' },
  122. testing: { emoji: '🧪', label: 'Test', color: 'yellow' },
  123.  
  124. // Build & Deploy
  125. build: { emoji: '🛠', label: 'Build', color: 'orange' },
  126. ci: { emoji: '⚙️', label: 'CI', color: 'gray' },
  127. cd: { emoji: '🚀', label: 'CD', color: 'gray' },
  128. deploy: { emoji: '📦', label: 'Deploy', color: 'orange' },
  129. release: { emoji: '🚀', label: 'Deploy', color: 'orange' },
  130.  
  131. // Maintenance
  132. chore: { emoji: '♻️', label: 'Chore', color: 'light-green' },
  133. deps: { emoji: '📦', label: 'Dependencies', color: 'light-green' },
  134. dep: { emoji: '📦', label: 'Dependencies', color: 'light-green' },
  135. dependencies: { emoji: '📦', label: 'Dependencies', color: 'light-green' },
  136. revert: { emoji: '🗑', label: 'Revert', color: 'red' },
  137. wip: { emoji: '🚧', label: 'WIP', color: 'dark-yellow' }
  138. }
  139. };
  140.  
  141. // Get saved configuration or use default
  142. const USER_CONFIG = GM_getValue('commitLabelsConfig', DEFAULT_CONFIG);
  143.  
  144. // Create configuration window
  145. function createConfigWindow() {
  146. const configWindow = document.createElement('div');
  147. configWindow.style.cssText = `
  148. position: fixed;
  149. top: 50%;
  150. left: 50%;
  151. transform: translate(-50%, -50%);
  152. background: #0d1117;
  153. border: 1px solid #30363d;
  154. border-radius: 6px;
  155. padding: 20px;
  156. z-index: 9999;
  157. width: 600px;
  158. max-height: 80vh;
  159. overflow-y: auto;
  160. color: #c9d1d9;
  161. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  162. `;
  163.  
  164. const title = document.createElement('h2');
  165. title.textContent = 'Commit Labels Configuration';
  166. title.style.marginBottom = '20px';
  167. configWindow.appendChild(title);
  168.  
  169. // Remove Prefix Option
  170. const prefixDiv = document.createElement('div');
  171. prefixDiv.style.marginBottom = '20px';
  172. const prefixCheckbox = document.createElement('input');
  173. prefixCheckbox.type = 'checkbox';
  174. prefixCheckbox.checked = USER_CONFIG.removePrefix;
  175. prefixCheckbox.id = 'remove-prefix';
  176. const prefixLabel = document.createElement('label');
  177. prefixLabel.htmlFor = 'remove-prefix';
  178. prefixLabel.textContent = ' Remove commit type prefix from message';
  179. prefixDiv.appendChild(prefixCheckbox);
  180. prefixDiv.appendChild(prefixLabel);
  181. configWindow.appendChild(prefixDiv);
  182.  
  183. // Commit Types Configuration
  184. const typesContainer = document.createElement('div');
  185. typesContainer.style.marginBottom = '20px';
  186.  
  187. // Group commit types by their label
  188. const groupedTypes = {};
  189. Object.entries(USER_CONFIG.commitTypes).forEach(([type, config]) => {
  190. const key = config.label;
  191. if (!groupedTypes[key]) {
  192. groupedTypes[key] = {
  193. types: [],
  194. config: config
  195. };
  196. }
  197. groupedTypes[key].types.push(type);
  198. });
  199.  
  200. // Create rows for grouped types
  201. Object.entries(groupedTypes).forEach(([label, { types, config }]) => {
  202. const typeDiv = document.createElement('div');
  203. typeDiv.style.marginBottom = '10px';
  204. typeDiv.style.display = 'flex';
  205. typeDiv.style.alignItems = 'center';
  206. typeDiv.style.gap = '10px';
  207.  
  208. // Type names (with aliases) and edit button container
  209. const typeContainer = document.createElement('div');
  210. typeContainer.style.display = 'flex';
  211. typeContainer.style.width = '150px';
  212. typeContainer.style.alignItems = 'center';
  213. typeContainer.style.gap = '4px';
  214.  
  215. const typeSpan = document.createElement('span');
  216. typeSpan.style.color = '#8b949e';
  217. typeSpan.style.flex = '1';
  218. typeSpan.textContent = types.join(', ') + ':';
  219.  
  220. const editAliasButton = document.createElement('button');
  221. editAliasButton.textContent = '✏️';
  222. editAliasButton.title = 'Edit Aliases';
  223. editAliasButton.style.cssText = `
  224. padding: 2px 4px;
  225. background: #21262d;
  226. color: #58a6ff;
  227. border: 1px solid #30363d;
  228. border-radius: 4px;
  229. cursor: pointer;
  230. font-size: 10px;
  231. `;
  232.  
  233. editAliasButton.onclick = () => {
  234. const currentAliases = types.join(', ');
  235. const newAliases = prompt('Edit aliases (separate with commas):', currentAliases);
  236.  
  237. if (newAliases && newAliases.trim()) {
  238. const newTypes = newAliases.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
  239.  
  240. // Check if any new aliases conflict with other types
  241. const conflictingType = newTypes.find(type =>
  242. USER_CONFIG.commitTypes[type] && !types.includes(type)
  243. );
  244.  
  245. if (conflictingType) {
  246. alert(`The alias "${conflictingType}" already exists in another group!`);
  247. return;
  248. }
  249.  
  250. // Remove old types
  251. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  252.  
  253. // Add new types with same config
  254. newTypes.forEach(type => {
  255. USER_CONFIG.commitTypes[type] = { ...config };
  256. });
  257.  
  258. // Update the display
  259. typeSpan.textContent = newTypes.join(', ') + ':';
  260.  
  261. // Update dataset for inputs
  262. const inputs = typeDiv.querySelectorAll('input, select');
  263. inputs.forEach(input => {
  264. input.dataset.types = newTypes.join(',');
  265. });
  266. }
  267. };
  268.  
  269. typeContainer.appendChild(typeSpan);
  270. typeContainer.appendChild(editAliasButton);
  271. typeDiv.appendChild(typeContainer);
  272.  
  273. // Emoji input
  274. const emojiInput = document.createElement('input');
  275. emojiInput.type = 'text';
  276. emojiInput.value = config.emoji;
  277. emojiInput.style.width = '40px';
  278. emojiInput.dataset.types = types.join(',');
  279. emojiInput.dataset.field = 'emoji';
  280. typeDiv.appendChild(emojiInput);
  281.  
  282. // Label input
  283. const labelInput = document.createElement('input');
  284. labelInput.type = 'text';
  285. labelInput.value = config.label;
  286. labelInput.style.width = '120px';
  287. labelInput.dataset.types = types.join(',');
  288. labelInput.dataset.field = 'label';
  289. typeDiv.appendChild(labelInput);
  290.  
  291. // Color select
  292. const colorSelect = document.createElement('select');
  293. Object.keys(COLORS).forEach(color => {
  294. const option = document.createElement('option');
  295. option.value = color;
  296. option.textContent = color;
  297. if (config.color === color) option.selected = true;
  298. colorSelect.appendChild(option);
  299. });
  300. colorSelect.dataset.types = types.join(',');
  301. colorSelect.dataset.field = 'color';
  302. typeDiv.appendChild(colorSelect);
  303.  
  304. // Delete button
  305. const deleteButton = document.createElement('button');
  306. deleteButton.textContent = '🗑️';
  307. deleteButton.style.cssText = `
  308. padding: 2px 8px;
  309. background: #21262d;
  310. color: #f85149;
  311. border: 1px solid #30363d;
  312. border-radius: 4px;
  313. cursor: pointer;
  314. `;
  315. deleteButton.onclick = () => {
  316. if (confirm(`Delete commit types "${types.join(', ')}"?`)) {
  317. typeDiv.remove();
  318. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  319. }
  320. };
  321. typeDiv.appendChild(deleteButton);
  322.  
  323. typesContainer.appendChild(typeDiv);
  324. });
  325.  
  326. // Add "Add New Type" button
  327. const addNewButton = document.createElement('button');
  328. addNewButton.textContent = '+ Add New Type';
  329. addNewButton.style.cssText = `
  330. margin-bottom: 15px;
  331. padding: 5px 16px;
  332. background: #238636;
  333. color: #fff;
  334. border: none;
  335. border-radius: 6px;
  336. cursor: pointer;
  337. `;
  338.  
  339. addNewButton.onclick = () => {
  340. const typeInput = prompt('Enter the commit type and aliases (separated by commas, e.g., "added, add"):', '');
  341. if (typeInput && typeInput.trim()) {
  342. const types = typeInput.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
  343.  
  344. // Check if any of the types already exist
  345. const existingType = types.find(type => USER_CONFIG.commitTypes[type]);
  346. if (existingType) {
  347. alert(`The commit type "${existingType}" already exists!`);
  348. return;
  349. }
  350.  
  351. // Create base config for all aliases
  352. const baseConfig = {
  353. emoji: '🔄',
  354. label: types[0].charAt(0).toUpperCase() + types[0].slice(1),
  355. color: 'blue'
  356. };
  357.  
  358. // Add all types to config with the same settings
  359. types.forEach(type => {
  360. USER_CONFIG.commitTypes[type] = { ...baseConfig };
  361. });
  362.  
  363. // Create and add new type row
  364. const typeDiv = document.createElement('div');
  365. typeDiv.style.marginBottom = '10px';
  366. typeDiv.style.display = 'flex';
  367. typeDiv.style.alignItems = 'center';
  368. typeDiv.style.gap = '10px';
  369.  
  370. // Type names (with aliases)
  371. const typeSpan = document.createElement('span');
  372. typeSpan.style.width = '150px';
  373. typeSpan.style.color = '#8b949e';
  374. typeSpan.textContent = types.join(', ') + ':';
  375. typeDiv.appendChild(typeSpan);
  376.  
  377. // Emoji input
  378. const emojiInput = document.createElement('input');
  379. emojiInput.type = 'text';
  380. emojiInput.value = baseConfig.emoji;
  381. emojiInput.style.width = '40px';
  382. emojiInput.dataset.types = types.join(',');
  383. emojiInput.dataset.field = 'emoji';
  384. typeDiv.appendChild(emojiInput);
  385.  
  386. // Label input
  387. const labelInput = document.createElement('input');
  388. labelInput.type = 'text';
  389. labelInput.value = baseConfig.label;
  390. labelInput.style.width = '120px';
  391. labelInput.dataset.types = types.join(',');
  392. labelInput.dataset.field = 'label';
  393. typeDiv.appendChild(labelInput);
  394.  
  395. // Color select
  396. const colorSelect = document.createElement('select');
  397. Object.keys(COLORS).forEach(color => {
  398. const option = document.createElement('option');
  399. option.value = color;
  400. option.textContent = color;
  401. if (color === 'blue') option.selected = true;
  402. colorSelect.appendChild(option);
  403. });
  404. colorSelect.dataset.types = types.join(',');
  405. colorSelect.dataset.field = 'color';
  406. typeDiv.appendChild(colorSelect);
  407.  
  408. // Delete button
  409. const deleteButton = document.createElement('button');
  410. deleteButton.textContent = '🗑️';
  411. deleteButton.style.cssText = `
  412. padding: 2px 8px;
  413. background: #21262d;
  414. color: #f85149;
  415. border: 1px solid #30363d;
  416. border-radius: 4px;
  417. cursor: pointer;
  418. `;
  419. deleteButton.onclick = () => {
  420. if (confirm(`Delete commit types "${types.join(', ')}"?`)) {
  421. typeDiv.remove();
  422. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  423. }
  424. };
  425. typeDiv.appendChild(deleteButton);
  426.  
  427. typesContainer.appendChild(typeDiv);
  428. }
  429. };
  430.  
  431. configWindow.appendChild(addNewButton);
  432. configWindow.appendChild(typesContainer);
  433.  
  434. // Save and Close buttons
  435. const buttonContainer = document.createElement('div');
  436. buttonContainer.style.display = 'flex';
  437. buttonContainer.style.gap = '10px';
  438. buttonContainer.style.justifyContent = 'flex-end';
  439.  
  440. const saveButton = document.createElement('button');
  441. saveButton.textContent = 'Save';
  442. saveButton.style.cssText = `
  443. padding: 5px 16px;
  444. background: #238636;
  445. color: #fff;
  446. border: none;
  447. border-radius: 6px;
  448. cursor: pointer;
  449. `;
  450.  
  451. const closeButton = document.createElement('button');
  452. closeButton.textContent = 'Close';
  453. closeButton.style.cssText = `
  454. padding: 5px 16px;
  455. background: #21262d;
  456. color: #c9d1d9;
  457. border: 1px solid #30363d;
  458. border-radius: 6px;
  459. cursor: pointer;
  460. `;
  461.  
  462. // Add Reset button next to Save and Close
  463. const resetButton = document.createElement('button');
  464. resetButton.textContent = 'Reset to Default';
  465. resetButton.style.cssText = `
  466. padding: 5px 16px;
  467. background: #21262d;
  468. color: #f85149;
  469. border: 1px solid #30363d;
  470. border-radius: 6px;
  471. cursor: pointer;
  472. margin-right: auto; // This pushes Save/Close to the right
  473. `;
  474.  
  475. resetButton.onclick = () => {
  476. if (confirm('Are you sure you want to reset all settings to default? This will remove all custom types and settings.')) {
  477. GM_setValue('commitLabelsConfig', DEFAULT_CONFIG);
  478. location.reload();
  479. }
  480. };
  481.  
  482. saveButton.onclick = () => {
  483. const newConfig = { ...USER_CONFIG };
  484. newConfig.removePrefix = prefixCheckbox.checked;
  485. newConfig.commitTypes = {};
  486.  
  487. typesContainer.querySelectorAll('input, select').forEach(input => {
  488. const types = input.dataset.types.split(',');
  489. const field = input.dataset.field;
  490.  
  491. types.forEach(type => {
  492. if (!newConfig.commitTypes[type]) {
  493. newConfig.commitTypes[type] = {};
  494. }
  495. newConfig.commitTypes[type][field] = input.value;
  496. });
  497. });
  498.  
  499. GM_setValue('commitLabelsConfig', newConfig);
  500. location.reload();
  501. };
  502.  
  503. closeButton.onclick = () => {
  504. document.body.removeChild(configWindow);
  505. };
  506.  
  507. buttonContainer.appendChild(resetButton);
  508. buttonContainer.appendChild(closeButton);
  509. buttonContainer.appendChild(saveButton);
  510. configWindow.appendChild(buttonContainer);
  511.  
  512. document.body.appendChild(configWindow);
  513. }
  514.  
  515. // Register configuration menu command
  516. GM_registerMenuCommand('Configure Commit Labels', createConfigWindow);
  517.  
  518. // Check if we're on a commit page
  519. function isCommitPage() {
  520. return window.location.pathname.includes('/commits') ||
  521. window.location.pathname.includes('/commit/');
  522. }
  523.  
  524. function addCommitLabels() {
  525. // Only proceed if we're on a commit page
  526. if (!isCommitPage()) return;
  527.  
  528. // Update selector to match GitHub's current DOM structure
  529. const commitMessages = document.querySelectorAll('.markdown-title a[data-pjax="true"]');
  530.  
  531. commitMessages.forEach(message => {
  532. const text = message.textContent.trim();
  533. const match = text.match(/^(\w+)(?:\([\w-]+\))?:\s*(.*)/);
  534.  
  535. if (match) {
  536. const type = match[1].toLowerCase();
  537. const restOfMessage = match[2];
  538.  
  539. if (USER_CONFIG.commitTypes[type]) {
  540. // Only add label if it hasn't been added yet
  541. if (!message.parentElement.querySelector('.commit-label')) {
  542. const label = document.createElement('span');
  543. label.className = 'commit-label';
  544. const color = COLORS[USER_CONFIG.commitTypes[type].color];
  545.  
  546. // Calculate perceived lightness (using GitHub's formula)
  547. const perceivedL = (color.l / 100);
  548. const lightnessSwitch = perceivedL <= 0.6 ? 1 : 0;
  549. const lightenBy = ((0.6 - perceivedL) * 100) * lightnessSwitch;
  550.  
  551. // Apply styles
  552. const styles = {
  553. ...USER_CONFIG.labelStyle,
  554. '--label-h': color.h,
  555. '--label-s': color.s,
  556. '--label-l': color.l,
  557. 'color': `hsl(${color.h}, ${color.s}%, ${color.l + lightenBy}%)`,
  558. 'background': `hsla(${color.h}, ${color.s}%, ${color.l}%, 0.18)`,
  559. 'borderColor': `hsla(${color.h}, ${color.s}%, ${color.l + lightenBy}%, 0.3)`,
  560. backgroundColor: color.bg,
  561. color: color.text
  562. };
  563.  
  564. label.style.cssText = Object.entries(styles)
  565. .map(([key, value]) => `${key.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${value}`)
  566. .join(';');
  567.  
  568. // Add hover effect
  569. label.addEventListener('mouseenter', () => {
  570. label.style.transform = 'translateY(-1px)';
  571. label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
  572. });
  573.  
  574. label.addEventListener('mouseleave', () => {
  575. label.style.transform = 'translateY(0)';
  576. label.style.boxShadow = styles.boxShadow;
  577. });
  578.  
  579. const emoji = document.createElement('span');
  580. emoji.style.marginRight = '4px';
  581. emoji.style.fontSize = '14px';
  582. emoji.style.lineHeight = '1';
  583. emoji.textContent = USER_CONFIG.commitTypes[type].emoji;
  584.  
  585. const labelText = document.createElement('span');
  586. labelText.textContent = USER_CONFIG.commitTypes[type].label;
  587.  
  588. label.appendChild(emoji);
  589. label.appendChild(labelText);
  590. message.parentElement.insertBefore(label, message);
  591.  
  592. // Update the commit message text to remove the type prefix if enabled
  593. if (USER_CONFIG.removePrefix) {
  594. message.textContent = restOfMessage;
  595. }
  596. }
  597. }
  598. }
  599. });
  600. }
  601.  
  602. // Only set up observers if we're on a commit page
  603. if (isCommitPage()) {
  604. // Initial run
  605. addCommitLabels();
  606.  
  607. // Watch for DOM changes
  608. const observer = new MutationObserver((mutations) => {
  609. for (const mutation of mutations) {
  610. if (mutation.addedNodes.length) {
  611. addCommitLabels();
  612. }
  613. }
  614. });
  615.  
  616. // Start observing the document with the configured parameters
  617. observer.observe(document.body, { childList: true, subtree: true });
  618. }
  619. })();