Greasy Fork is available in English.

Gitea: copy commit reference

Adds a "Copy commit reference" button to every commit page on Gitea and Forgejo websites.

安裝腳本?
作者推薦腳本

您可能也會喜歡 GitLab: copy commit reference

安裝腳本
  1. // ==UserScript==
  2. // @name Gitea: copy commit reference
  3. // @namespace https://andrybak.dev
  4. // @version 11
  5. // @license AGPL-3.0-only
  6. // @author Andrei Rybak
  7. // @description Adds a "Copy commit reference" button to every commit page on Gitea and Forgejo websites.
  8. // @icon https://about.gitea.com/favicon.ico
  9. // @homepageURL https://github.com/rybak/copy-commit-reference-userscript
  10. // @supportURL https://github.com/rybak/copy-commit-reference-userscript/issues
  11. // @match https://gitea.com/*/commit/*
  12. // @match https://git.plastiras.org/*/commit/*
  13. // @match https://projects.blender.org/*/commit/*
  14. // @match https://codeberg.org/*/commit/*
  15. // @match https://next.forgejo.org/*/commit/*
  16. // @match https://code.forgejo.org/*/commit/*
  17. // @require https://cdn.jsdelivr.net/gh/rybak/userscript-libs@e86c722f2c9cc2a96298c8511028f15c45180185/waitForElement.js
  18. // @require https://cdn.jsdelivr.net/gh/rybak/copy-commit-reference-userscript@4f71749bc0d302d4ff4a414b0f4a6eddcc6a56ad/copy-commit-reference-lib.js
  19. // @grant none
  20. // ==/UserScript==
  21.  
  22. /*
  23. * Copyright (C) 2023-2025 Andrei Rybak
  24. *
  25. * This program is free software: you can redistribute it and/or modify
  26. * it under the terms of the GNU Affero General Public License as published
  27. * by the Free Software Foundation, version 3.
  28. *
  29. * This program is distributed in the hope that it will be useful,
  30. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  31. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  32. * GNU Affero General Public License for more details.
  33. *
  34. * You should have received a copy of the GNU Affero General Public License
  35. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  36. */
  37.  
  38. (function () {
  39. 'use strict';
  40.  
  41. /**
  42. * Implementation for Gitea and its fork Forgejo.
  43. *
  44. * Example URLs for testing:
  45. * - https://git.plastiras.org/Tha_14/Antidote/commit/f84a08f1ac5312acc9ccedff25e6957e575f03ff
  46. * - https://codeberg.org/forgejo/forgejo/commit/e35d92de1f37bea0a593093678f093845955e3fc
  47. * - https://codeberg.org/forgejo/forgejo/commit/a4369782e1cfbbc6f588c0cda5776ee823b0e493
  48. */
  49. class Gitea extends GitHosting {
  50. getTargetSelector() {
  51. return '.commit-header h3 + div';
  52. }
  53.  
  54. wrapButtonContainer(container) {
  55. container.style.marginRight = '0.5rem';
  56. return container;
  57. }
  58.  
  59. wrapButton(button) {
  60. /*
  61. * Mimicking Gitea's "Browse Source" button, but without class 'primary',
  62. * because there shouldn't be too many primary buttons. Class 'basic' is
  63. * for styling like most of the buttons on the commit pages.
  64. */
  65. button.classList.add('ui', 'tiny', 'button', 'basic');
  66. const maybeNativeIcon = document.querySelector('.svg.octicon-copy');
  67. if (maybeNativeIcon) {
  68. /*
  69. * Some instances of Gitea don't have the copy icons,
  70. * e.g. https://projects.blender.org
  71. */
  72. const icon = maybeNativeIcon.cloneNode(true);
  73. icon.style.verticalAlign = 'middle';
  74. icon.style.marginTop = '-4px';
  75. button.insertBefore(document.createTextNode(" "), button.childNodes[0]);
  76. button.insertBefore(icon, button.childNodes[0]);
  77. }
  78. return button;
  79. }
  80.  
  81. addButtonContainerToTarget(target, buttonContainer) {
  82. // to the left of Gitea's "Browse Source" button
  83. target.insertBefore(buttonContainer, target.querySelector('.ui.primary.tiny.button'));
  84. }
  85.  
  86. /**
  87. * Styles adapted from GitHub's CSS classes ".tooltipped::before"
  88. * and ".tooltipped-s::before".
  89. *
  90. * @returns {HTMLElement}
  91. */
  92. #createTooltipTriangle() {
  93. const triangle = document.createElement('div');
  94. triangle.style.position = 'absolute';
  95. triangle.style.zIndex = '1000001';
  96. triangle.style.bottom = '-15px'; // not -16px to look better at different zoom levels
  97. triangle.style.left = '14px'; // to align with .left of `checkmark`
  98. triangle.style.height = '0';
  99. triangle.style.width = '0';
  100. /*
  101. * Borders connect at 45° angle => when only top border is colored,
  102. * it's a trapezoid. But with width=0, the bottom edge of trapezoid
  103. * has length 0, so it's a downwards triangle.
  104. *
  105. * bgColor from Gitea CSS classes
  106. */
  107. triangle.style.border = '8px solid transparent';
  108. triangle.style.borderTopColor = 'var(--color-tooltip-bg)';
  109. return triangle;
  110. }
  111.  
  112. createCheckmark() {
  113. const checkmark = super.createCheckmark();
  114. checkmark.style.left = '0.2rem'; // to put emoji right above the button's icon
  115. checkmark.style.bottom = 'calc(100% + 1.2rem)'; // to mimic native tooltips shown above the buttons
  116. /*
  117. * Look and feel from CSS classes of Tippy -- a library (?)
  118. * used by Gitea.
  119. */
  120. checkmark.style.zIndex = '9999';
  121. checkmark.style.backgroundColor = 'var(--color-tooltip-bg)';
  122. checkmark.style.color = 'var(--color-tooltip-text)';
  123. checkmark.style.borderRadius = 'var(--border-radius)';
  124. checkmark.style.fontSize = '1rem';
  125. checkmark.style.padding = '.5rem 1rem';
  126. checkmark.appendChild(this.#createTooltipTriangle());
  127. return checkmark;
  128. }
  129.  
  130. getFullHash() {
  131. const browseButton = document.querySelector('.commit-header h3 + div > a');
  132. const lastSlashIndex = browseButton.href.lastIndexOf('/');
  133. return browseButton.href.slice(lastSlashIndex + 1);
  134. }
  135.  
  136. getDateIso(hash) {
  137. const timeTag = document.querySelector('#authored-time relative-time');
  138. return timeTag.datetime.slice(0, 'YYYY-MM-DD'.length);
  139. }
  140.  
  141. getCommitMessage(hash) {
  142. const subj = document.querySelector('.commit-summary').innerText;
  143. const bodyElement = document.querySelector('.commit-body');
  144. if (!bodyElement) {
  145. return subj;
  146. }
  147. const body = bodyElement.childNodes[0].innerText;
  148. return subj + '\n\n' + body;
  149. }
  150.  
  151. static #getIssuesUrl() {
  152. const oldUiElement = document.querySelector('.header-wrapper > .ui.tabs.container > .tabular.menu.navbar a[href$="/issues"]');
  153. if (oldUiElement) {
  154. return oldUiElement.href;
  155. }
  156. return document.querySelector('.header-wrapper .navbar a[href$="/issues"], .secondary-nav .overflow-menu-items a[href$="/issues"]').href;
  157. }
  158.  
  159. async convertPlainSubjectToHtml(plainTextSubject, hash) {
  160. const escapedHtml = await super.convertPlainSubjectToHtml(plainTextSubject, hash);
  161. if (!escapedHtml.includes('#')) {
  162. return escapedHtml;
  163. }
  164. const issuesUrl = Gitea.#getIssuesUrl();
  165. return escapedHtml.replaceAll(/(?<!&)#([0-9]+)/g, `<a href="${issuesUrl}/\$1">#\$1</a>`);
  166. }
  167. }
  168.  
  169. CopyCommitReference.runForGitHostings(new Gitea());
  170. })();