GitLab: copy commit reference

Adds a "Copy commit reference" button to every commit page on GitLab.

安装此脚本?
作者推荐脚本

您可能也喜欢Gitea: copy commit reference

安装此脚本
  1. // ==UserScript==
  2. // @name GitLab: copy commit reference
  3. // @namespace https://andrybak.dev
  4. // @license AGPL-3.0-only
  5. // @version 9
  6. // @description Adds a "Copy commit reference" button to every commit page on GitLab.
  7. // @homepageURL https://gitlab.com/andrybak/copy-commit-reference-userscript
  8. // @supportURL https://gitlab.com/andrybak/copy-commit-reference-userscript/-/issues
  9. // @author Andrei Rybak
  10. // @match https://gitlab.com/*/-/commit/*
  11. // @match https://invent.kde.org/*/-/commit/*
  12. // @match https://gitlab.gnome.org/*/-/commit/*
  13. // @icon https://gitlab.com/assets/favicon-72a2cad5025aa931d6ea56c3201d1f18e68a8cd39788c7c80d5b2b82aa5143ef.png
  14. // @require https://cdn.jsdelivr.net/gh/rybak/userscript-libs@e86c722f2c9cc2a96298c8511028f15c45180185/waitForElement.js
  15. // @require https://cdn.jsdelivr.net/gh/rybak/copy-commit-reference-userscript@4f71749bc0d302d4ff4a414b0f4a6eddcc6a56ad/copy-commit-reference-lib.js
  16. // @grant none
  17. // ==/UserScript==
  18.  
  19. /*
  20. * Copyright (C) 2023-2025 Andrei Rybak
  21. *
  22. * This program is free software: you can redistribute it and/or modify
  23. * it under the terms of the GNU Affero General Public License as published
  24. * by the Free Software Foundation, version 3.
  25. *
  26. * This program is distributed in the hope that it will be useful,
  27. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  28. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  29. * GNU Affero General Public License for more details.
  30. *
  31. * You should have received a copy of the GNU Affero General Public License
  32. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  33. */
  34.  
  35. (function () {
  36. 'use strict';
  37.  
  38. /*
  39. * Implementation for GitLab.
  40. *
  41. * Example URLs for testing:
  42. * - https://gitlab.com/andrybak/resoday/-/commit/b82824ec6dc3f14c3711104bf0ffd792c86d19ba
  43. * - https://invent.kde.org/education/kturtle/-/commit/8beecff6f76a4afc74879c46517d00657d8426f9
  44. */
  45. class GitLab extends GitHosting {
  46. static #HEADER_SELECTOR = 'main#content-body .page-content-header > .header-main-content';
  47.  
  48. getTargetSelector() {
  49. return GitLab.#HEADER_SELECTOR;
  50. }
  51.  
  52. getButtonTagName() {
  53. return 'button'; // like GitLab's "Copy commit SHA"
  54. }
  55.  
  56. wrapButton(button) {
  57. const copyShaButtonIcon = document.querySelector(`${GitLab.#HEADER_SELECTOR} > button > svg[data-testid="copy-to-clipboard-icon"]`);
  58. const icon = copyShaButtonIcon.cloneNode(true);
  59. button.replaceChildren(icon); // is just icon enough?
  60. button.classList.add('btn-sm', 'btn-default', 'btn-default-tertiary', 'btn-icon', 'btn', 'btn-clipboard', 'gl-button');
  61. button.setAttribute('data-toggle', 'tooltip'); // this is needed to have a fancy tooltip in style of other UI
  62. button.setAttribute('data-placement', 'bottom'); // this is needed so that the fancy tooltip appears below the button
  63. button.style = 'border: 1px solid darkgray;';
  64. button.title = this.getButtonText() + " to clipboard";
  65. return button;
  66. }
  67.  
  68. getFullHash() {
  69. const copyShaButton = document.querySelector(`${GitLab.#HEADER_SELECTOR} > button`);
  70. return copyShaButton.getAttribute('data-clipboard-text');
  71. }
  72.  
  73. getDateIso(hash) {
  74. // careful not to select <time> tag for "Committed by"
  75. const authorTimeTag = document.querySelector(`${GitLab.#HEADER_SELECTOR} > span + time`);
  76. return authorTimeTag.getAttribute('datetime').slice(0, 'YYYY-MM-DD'.length);
  77. }
  78.  
  79. getCommitMessage(hash) {
  80. /*
  81. * Even though vast majority will only need `subj`, gather everything and
  82. * let downstream code handle paragraph splitting.
  83. */
  84. const subj = document.querySelector('.commit-box .commit-title').innerText;
  85. const maybeBody = document.querySelector('.commit-box .commit-description');
  86. if (maybeBody == null) { // some commits have only a single-line message
  87. return subj;
  88. }
  89. const body = maybeBody.innerText;
  90. return subj + '\n\n' + body;
  91. }
  92.  
  93. addButtonContainerToTarget(target, buttonContainer) {
  94. const authoredSpanTag = target.querySelector('span.d-sm-inline');
  95. target.insertBefore(buttonContainer, authoredSpanTag);
  96. // add spacer to make text "authored" not stick to the button
  97. target.insertBefore(document.createTextNode(" "), authoredSpanTag);
  98. }
  99.  
  100. /*
  101. * GitLab has a complex interaction with library ClipboardJS:
  102. *
  103. * - https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/helpers/button_helper.rb#L31-68
  104. * - https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/behaviors/copy_to_clipboard.js#L63-94
  105. *
  106. * and the native tooltips are even more complicated.
  107. */
  108. createCheckmark() {
  109. const checkmark = super.createCheckmark();
  110. checkmark.style.left = 'calc(100% + 0.3rem)';
  111. checkmark.style.lineHeight = '1.5';
  112. checkmark.style.padding = '0.5rem 1.5rem';
  113. checkmark.style.textAlign = 'center';
  114. checkmark.style.width = 'auto';
  115. checkmark.style.borderRadius = '3px';
  116. checkmark.style.fontSize = '0.75rem';
  117. checkmark.style.fontFamily = '"Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
  118. if (document.body.classList.contains('gl-dark')) {
  119. checkmark.style.backgroundColor = '#dcdcde';
  120. checkmark.style.color = '#1f1e24';
  121. } else {
  122. checkmark.style.backgroundColor = '#000';
  123. checkmark.style.color = '#fff';
  124. }
  125. return checkmark;
  126. }
  127.  
  128. /**
  129. * @returns {string}
  130. */
  131. static #getIssuesUrl() {
  132. const newUiIssuesLink = document.querySelector('nav a[href$="/issues"]');
  133. if (newUiIssuesLink) {
  134. return newUiIssuesLink.href;
  135. }
  136. const oldUiIssuesLink = document.querySelector('aside a[href$="/issues"]');
  137. return oldUiIssuesLink.href;
  138. }
  139.  
  140. async convertPlainSubjectToHtml(plainTextSubject, commitHash) {
  141. const escapedHtml = await super.convertPlainSubjectToHtml(plainTextSubject, commitHash);
  142. if (!escapedHtml.includes('#')) {
  143. return escapedHtml;
  144. }
  145. const issuesUrl = GitLab.#getIssuesUrl();
  146. return escapedHtml.replaceAll(/(?<!&)#([0-9]+)/g, `<a href="${issuesUrl}/\$1">#\$1</a>`);
  147. }
  148. }
  149.  
  150. CopyCommitReference.runForGitHostings(new GitLab());
  151. })();