Enhance some features of the Self-managed Gitlab

Enhance some features of the Self-managed Gitlab, such as the CI/CD settings page, the merge request create/edit page etc.

  1. // ==UserScript==
  2. // @name Enhance some features of the Self-managed Gitlab
  3. // @name:zh 强化自托管 Gitlab 能力
  4. // @name:zh-CN 强化自托管 Gitlab 能力
  5. // @name:zh-Hans 强化自托管 Gitlab 能力
  6. // @name:zh-TW 強化自託管 Gitlab 能力
  7. // @name:zh-HK 強化自託管 Gitlab 能力
  8. // @name:zh-Hant 強化自託管 Gitlab 能力
  9. // @namespace https://greasyfork.org/users/1133279
  10. // @description Enhance some features of the Self-managed Gitlab, such as the CI/CD settings page, the merge request create/edit page etc.
  11. // @description:zh 强化自托管 Gitlab 的一些功能,如 CI/CD 设置页面、合并请求创建/编辑页面等。
  12. // @description:zh-CN 强化自托管 Gitlab 的一些功能,如 CI/CD 设置页面、合并请求创建/编辑页面等。
  13. // @description:zh-Hans 强化自托管 Gitlab 的一些功能,如 CI/CD 设置页面、合并请求创建/编辑页面等。
  14. // @description:zh-TW 強化自託管 Gitlab 的一些功能,如 CI/CD 設置頁面、合併請求創建/編輯頁面等。
  15. // @description:zh-HK 強化自託管 Gitlab 的一些功能,如 CI/CD 設置頁面、合併請求創建/編輯頁面等。
  16. // @description:zh-Hant 強化自託管 Gitlab 的一些功能,如 CI/CD 設置頁面、合併請求創建/編輯頁面等。
  17. // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGhlaWdodD0iNTEycHgiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDUxMiA1MTI7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEycHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxnIGlkPSJfeDMxXzQ0LWdpdGxhYiI+PGc+PGcgaWQ9IlhNTElEXzZfIj48Zz48Zz48cGF0aCBkPSJNNTIuNzUyLDIwNS41MDFsMjAzLjE4LDI2NC4wN2wtMjIyLjctMTY1LjI5Yy02LjExLTQuNTktOC43Mi0xMi41OC02LjM4LTE5Ljc3MWwyNS44Ny03OS4wNSAgICAgICBMNTIuNzUyLDIwNS41MDF6IiBzdHlsZT0iZmlsbDojRkNBMzI2OyIvPjwvZz48Zz48cG9seWdvbiBwb2ludHM9IjE3MS4zMDIsMjA1LjQ2MSAyNTYuMDEyLDQ2OS41NDEgMjU1LjkzMiw0NjkuNTcxIDUyLjc1MiwyMDUuNTAxIDUyLjgxMiwyMDUuNDYxICAgICAgICAgICAgICIgc3R5bGU9ImZpbGw6I0ZDNkQyNjsiLz48L2c+PGc+PHBvbHlnb24gcG9pbnRzPSIzNDAuNzMxLDIwNS40NjEgMjU2LjAyMSw0NjkuNTcxIDI1Ni4wMTIsNDY5LjU0MSAxNzEuMzAyLDIwNS40NjEgMTcxLjM5MiwyMDUuNDYxICAgICAgICAzNDAuNjQyLDIwNS40NjEgICAgICAiIHN0eWxlPSJmaWxsOiNFMjQzMjk7Ii8+PC9nPjxnPjxwb2x5Z29uIHBvaW50cz0iNDU5LjI5MiwyMDUuNTAxIDI1Ni4wMjEsNDY5LjU3MSAzNDAuNzMxLDIwNS40NjEgNDU5LjIzMSwyMDUuNDYxICAgICAgIiBzdHlsZT0iZmlsbDojRkM2RDI2OyIvPjwvZz48Zz48cGF0aCBkPSJNNDg1LjE5MSwyODQuNTExYzIuMjQsNy4xOS0wLjI3LDE1LjE4MS02LjQ3LDE5Ljc3MWwtMjIyLjcsMTY1LjI5bDIwMy4yNzEtMjY0LjA3bDAuMDI5LTAuMDQgICAgICAgTDQ4NS4xOTEsMjg0LjUxMXoiIHN0eWxlPSJmaWxsOiNGQ0EzMjY7Ii8+PC9nPjxnPjxwYXRoIGQ9Ik00MDguNDcyLDQ4LjQyMWw1MC43NiwxNTcuMDRoLTExOC41aC0wLjA5bDUwLjg1LTE1Ny4wNCAgICAgICBDMzk0LjM2MSw0MC40MzEsNDA1LjY4Miw0MC40MzEsNDA4LjQ3Miw0OC40MjF6IiBzdHlsZT0iZmlsbDojRTI0MzI5OyIvPjwvZz48Zz48cGF0aCBkPSJNMTcxLjM5MiwyMDUuNDYxaC0wLjA5SDUyLjgxMmw1MC43Ni0xNTcuMDRjMi44Ny03Ljk5LDE0LjE5LTcuOTksMTYuOTgsMCAgICAgICBDMTIwLjU1Miw0OC40MjEsMTcxLjMwMiwyMDUuNDYxLDE3MS4zOTIsMjA1LjQ2MXoiIHN0eWxlPSJmaWxsOiNFMjQzMjk7Ii8+PC9nPjwvZz48L2c+PC9nPjwvZz48ZyBpZD0iTGF5ZXJfMSIvPjwvc3ZnPg==
  18. // @version 12
  19. // @author Arylo
  20. // @include /^https:\/\/(git(lab)?|code)\.[^/]+\/.*\/-\/settings\/ci_cd$/
  21. // @include /^https:\/\/(git(lab)?|code)\.[^/]+\/.*\/-\/merge_requests\/new\b/
  22. // @include /^https:\/\/(git(lab)?|code)\.[^/]+\/.*\/-\/merge_requests\/\d+/edit\b/
  23. // @include /^https:\/\/(git(lab)?|code)\.[^/]+\/dashboard\/merge_requests\b/
  24. // @license MIT
  25. // @homepage https://greasyfork.org/zh-CN/scripts/519026
  26. // @supportURL https://greasyfork.org/zh-CN/scripts/519026/feedback
  27. // @run-at document-end
  28. // @grant GM_addStyle
  29. // @grant GM_setClipboard
  30. // ==/UserScript==
  31. "use strict";
  32. (() => {
  33. // src/monkey/polyfill/GM.ts
  34. var thisGlobal = window;
  35. if (typeof thisGlobal.GM === "undefined") {
  36. thisGlobal.GM = {};
  37. }
  38. function getGMWindow() {
  39. return thisGlobal;
  40. }
  41.  
  42. // src/monkey/polyfill/GM_addStyle.ts
  43. var w = getGMWindow();
  44. if (typeof w.GM_addStyle === "undefined") {
  45. w.GM_addStyle = function GM_addStyle2(cssContent) {
  46. const head = document.getElementsByTagName("head")[0];
  47. if (head) {
  48. const styleElement = document.createElement("style");
  49. styleElement.setAttribute("type", "text/css");
  50. styleElement.textContent = cssContent;
  51. head.appendChild(styleElement);
  52. return styleElement;
  53. }
  54. return null;
  55. };
  56. }
  57. if (typeof w.GM.addStyle === "undefined") {
  58. w.GM.addStyle = GM_addStyle;
  59. }
  60.  
  61. // src/monkey/gitlab-enhance/settings/ci_cd.css
  62. var ci_cd_default = ".content-wrapper nav{max-width:100%}.content-wrapper .container-fluid{max-width:100%}.ci-variable-table table colgroup col:nth-child(3){width:100px}.ci-variable-table table colgroup col:nth-child(4){width:200px}.ci-variable-table table colgroup col:nth-child(5){width:50px}\n";
  63.  
  64. // src/monkey/gitlab-enhance/settings/ci_cd.ts
  65. if (location.pathname.endsWith("/-/settings/ci_cd")) {
  66. setTimeout(() => GM_addStyle(ci_cd_default), 25);
  67. }
  68.  
  69. // src/monkey/gitlab-enhance/dashboard/merge_requests.ts
  70. var hyperlinkResource = () => {
  71. const mergeRequests = $(".merge-request:not([hyperlinked])");
  72. mergeRequests.each((_, mergeRequestEle) => {
  73. const href = $(".js-prefetch-document", mergeRequestEle).attr("href");
  74. if (!href) return;
  75. const resourceUrl = href.replace(/\/-\/merge_requests\/\d+$/, "");
  76. const rawRefEle = $(".issuable-reference", mergeRequestEle);
  77. const rawRefName = rawRefEle.text();
  78. const [resourceName, number] = rawRefName.split("!");
  79. rawRefEle.html(`<a href="${resourceUrl}">${resourceName}</a>!${number}`);
  80. $(mergeRequestEle).attr("hyperlinked", "");
  81. });
  82. };
  83. if (location.pathname.endsWith("/dashboard/merge_requests")) {
  84. $(".issuable-list").on("mouseenter", hyperlinkResource);
  85. setTimeout(hyperlinkResource, 1e3);
  86. }
  87.  
  88. // src/monkey/gitlab-enhance/utils.ts
  89. var getButtonElement = (text2) => {
  90. const classnames = "gl-font-sm! gl-ml-3 gl-button btn btn-default btn-sm";
  91. return $(`<a class="${classnames}">${text2}</a>`);
  92. };
  93.  
  94. // packages/MdGenerator/MdTools.ts
  95. var INDENT_SPACE_LENGTH = 2;
  96. function indent(level, content = "") {
  97. return `${Array((level - 1) * INDENT_SPACE_LENGTH).fill(" ").join("")}${content}`;
  98. }
  99. function enter() {
  100. return "\n";
  101. }
  102. var header = (level) => (text2 = "") => `${Array(level).fill("#").join("")} ${text2}`.trim();
  103. function h1(...args) {
  104. return header(1)(...args);
  105. }
  106. function h2(...args) {
  107. return header(2)(...args);
  108. }
  109. function h3(...args) {
  110. return header(3)(...args);
  111. }
  112. function h4(...args) {
  113. return header(4)(...args);
  114. }
  115. function h5(...args) {
  116. return header(5)(...args);
  117. }
  118. function text(content = "") {
  119. return content;
  120. }
  121. function anchor(key, url) {
  122. return `[${key}]: ${url}`;
  123. }
  124. function hyperlink(label, url) {
  125. return `[${label}](${url})`;
  126. }
  127. function hyperlinkWithKey(label, key) {
  128. return `[${label}][${key}]`;
  129. }
  130. function image(url, alt = "") {
  131. return `!${hyperlink(alt, url)}`;
  132. }
  133. function imageByKey(key, alt = "") {
  134. return `!${hyperlinkWithKey(alt, key)}`;
  135. }
  136. function listItem(text2 = "") {
  137. return `- ${text2}`.trimEnd();
  138. }
  139. function taskItem(text2 = "", { selected = false } = {}) {
  140. return `[${selected ? "x" : " "}] ${text2}`.trimEnd();
  141. }
  142.  
  143. // packages/MdGenerator/index.ts
  144. function getOption(options, key, defaultValue) {
  145. return options[key] ?? defaultValue;
  146. }
  147. function getLevelFromOptions(options = {}) {
  148. const { level = 1 } = options;
  149. return level;
  150. }
  151. function cloneDeep(value) {
  152. return JSON.parse(JSON.stringify(value));
  153. }
  154. var Template = class {
  155. constructor(text2) {
  156. this.anchorMap = {};
  157. this.templateContent = text2;
  158. this.headerNextUtils = {
  159. text: this.text.bind(this),
  160. image: this.image.bind(this),
  161. hyperlink: this.hyperlink.bind(this),
  162. listItem: this.listItem.bind(this),
  163. taskItem: this.taskItem.bind(this)
  164. };
  165. this.contentNextUtils = {
  166. ...this.headerNextUtils,
  167. end: this.emptyLine.bind(this)
  168. };
  169. }
  170. h1(text2) {
  171. this.text(h1(text2));
  172. this.emptyLine();
  173. return this.headerNextUtils;
  174. }
  175. h2(text2) {
  176. this.text(h2(text2));
  177. this.emptyLine();
  178. return this.headerNextUtils;
  179. }
  180. h3(text2) {
  181. this.text(h3(text2));
  182. this.emptyLine();
  183. return this.headerNextUtils;
  184. }
  185. h4(text2) {
  186. this.text(h4(text2));
  187. this.emptyLine();
  188. return this.headerNextUtils;
  189. }
  190. h5(text2) {
  191. this.text(h5(text2));
  192. this.emptyLine();
  193. return this.headerNextUtils;
  194. }
  195. [Symbol.toStringTag]() {
  196. return [
  197. this.templateContent,
  198. this.templateContent && !/\n$/.test(this.templateContent) && enter(),
  199. Object.keys(this.anchorMap).length && enter(),
  200. Object.entries(this.anchorMap).map(([key, value]) => anchor(key, value)).join(enter()),
  201. Object.keys(this.anchorMap).length && enter()
  202. ].filter(Boolean).join("").replace(/\n(\s*\n){2,}/g, "\n\n");
  203. }
  204. text(content = "", opts) {
  205. this.templateContent += indent(getLevelFromOptions(opts), `${text(content)}${enter()}`);
  206. return this.contentNextUtils;
  207. }
  208. listItem(text2 = "", opts) {
  209. return this.text(listItem(text2), { level: getLevelFromOptions(opts) });
  210. }
  211. taskItem(text2 = "", opts) {
  212. return this.listItem(taskItem(text2, { selected: getOption(opts || {}, "selected", false) }), { level: getLevelFromOptions(opts) });
  213. }
  214. hyperlink(text2, link, opts) {
  215. let content;
  216. const anchorKey = getOption(opts || {}, "anchorKey", "");
  217. if (typeof anchorKey === "string" && anchorKey.length) {
  218. anchor(anchorKey, link);
  219. content = hyperlinkWithKey(text2, anchorKey);
  220. } else {
  221. content = hyperlink(text2, link);
  222. }
  223. return this.text(content, { level: getLevelFromOptions(opts) });
  224. }
  225. image(url, opts) {
  226. let content;
  227. const anchorKey = getOption(opts || {}, "anchorKey", "");
  228. const alt = getOption(opts || {}, "alt", "");
  229. if (typeof anchorKey === "string" && anchorKey.length) {
  230. this.anchor(anchorKey, url);
  231. content = imageByKey(alt, anchorKey);
  232. } else {
  233. content = image(alt, url);
  234. }
  235. return this.text(content, { level: getLevelFromOptions(opts) });
  236. }
  237. table(opts) {
  238. const tableMap = { header: [], body: [] };
  239. const actionMap = {
  240. header: (row) => {
  241. row.forEach(({ key, title }) => {
  242. tableMap.header.push({ key, title });
  243. });
  244. return actionMap;
  245. },
  246. body: (row) => {
  247. tableMap.body.push(row);
  248. return actionMap;
  249. },
  250. end: () => {
  251. if (!tableMap.header.length) return "";
  252. const { header: header2, body } = tableMap;
  253. const headerContent = header2.map(({ title }) => `|${title}`).join("") + "|";
  254. this.text(headerContent, { level: getLevelFromOptions(opts) });
  255. const separator = header2.map(() => "|--").join("") + "|";
  256. this.text(separator, { level: getLevelFromOptions(opts) });
  257. body.forEach((row) => {
  258. const content = header2.map(({ key }) => `|${(row[key] ?? "").toString().replace(/\|/g, "|")}`).join("") + "|";
  259. this.text(content, { level: getLevelFromOptions(opts) });
  260. });
  261. }
  262. };
  263. return actionMap;
  264. }
  265. anchor(key, link) {
  266. this.anchorMap[key] = link;
  267. }
  268. emptyLine() {
  269. if (!this.templateContent.endsWith(enter())) {
  270. this.templateContent += enter();
  271. }
  272. this.templateContent += enter();
  273. }
  274. modify(callback) {
  275. this.templateContent = callback(cloneDeep(this.templateContent));
  276. return this.contentNextUtils;
  277. }
  278. };
  279. var genTemplate = (callback = () => {
  280. }) => {
  281. return readTemplate("", callback);
  282. };
  283. var readTemplate = (text2, callback = () => {
  284. }) => {
  285. const templateInst = new Template(cloneDeep(text2));
  286. callback(templateInst);
  287. return templateInst[Symbol.toStringTag]();
  288. };
  289.  
  290. // src/monkey/gitlab-enhance/merge_requests/template.css
  291. var template_default = ".gl-display-flex:has([for=merge_request_description]){align-items:baseline}\n";
  292.  
  293. // src/monkey/gitlab-enhance/merge_requests/template.ts
  294. var getBranchType = () => {
  295. const fromBranchName = $(".align-self-center code:not([data-branch-name])").text();
  296. const prefixBranchName = fromBranchName.split("/")[0].toLowerCase();
  297. switch (prefixBranchName) {
  298. case "feature":
  299. case "feat":
  300. return 0 /* FEATURE */;
  301. case "fix":
  302. case "bugfix":
  303. return 1 /* BUGFIX */;
  304. case "hotfix":
  305. return 2 /* HOTFIX */;
  306. case "devops":
  307. case "chore":
  308. case "test":
  309. case "doc":
  310. case "docs":
  311. return 3 /* TASKS */;
  312. default:
  313. return 4 /* OTHERS */;
  314. }
  315. };
  316. var generateTemplate = () => {
  317. const branchType = getBranchType();
  318. return genTemplate((utils) => {
  319. utils.h2("Type").taskItem("Feature (Story/Refactor)", { selected: branchType === 0 /* FEATURE */ }).taskItem(`Bugfix`, { selected: branchType === 1 /* BUGFIX */ }).taskItem(`Hotfix (Production Issues)`, { selected: branchType === 2 /* HOTFIX */ }).taskItem(`Tasks (DevOps / Unit Test / Document Update)`, { selected: branchType === 3 /* TASKS */ }).taskItem(`Others`, { selected: branchType === 4 /* OTHERS */ }).end();
  320. utils.h2("Description");
  321. if (branchType !== 0 /* FEATURE */) {
  322. utils.h3("Why (Why does this happen?)").listItem().end();
  323. utils.h3("How (How can we avoid or solve it?)").listItem().end();
  324. }
  325. utils.h3("What (What did you do this time?)").listItem().end();
  326. if (branchType !== 3 /* TASKS */) {
  327. utils.h3("Results (Screenshot, etc)");
  328. utils.h4("Before modification");
  329. utils.h4("After modification");
  330. utils.h2("Affected Zone").listItem("Affected Module(s):").listItem("Affected URL(s):").end();
  331. }
  332. utils.h2("External resources (Mention, Resolves, or Closes)");
  333. });
  334. };
  335. var appendTemplateButton = () => {
  336. GM_addStyle(template_default);
  337. const text2 = "Copy Template";
  338. const btnElement = getButtonElement(text2);
  339. const templateContent = generateTemplate();
  340. const hint = $('<span class="gl-font-sm! gl-ml-3 gl-text-secondary"></span>');
  341. let setTimeoutId;
  342. btnElement.on("click", async () => {
  343. hint.remove();
  344. setTimeoutId && clearTimeout(setTimeoutId);
  345. hint.text("Copying...");
  346. btnElement.after(hint);
  347. await GM_setClipboard(templateContent, "text", () => {
  348. hint.text("Copied!");
  349. setTimeoutId = setTimeout(() => hint.remove(), 3e3);
  350. });
  351. });
  352. $("*:has(>[for=merge_request_description])").append(btnElement);
  353. };
  354.  
  355. // src/monkey/gitlab-enhance/merge_requests/new.ts
  356. var appendAsTitleButton = () => {
  357. $(".commit-content").each((_, el) => {
  358. const titleElements = $(".item-title", el);
  359. const title = titleElements.text();
  360. const btnElement = getButtonElement("As title");
  361. btnElement.on("click", () => {
  362. $("input[data-testid=issuable-form-title-field]").val(title);
  363. $("input[data-testid=issuable-form-title-field]").focus();
  364. });
  365. $(".committer", el).before(btnElement);
  366. });
  367. };
  368. if (location.pathname.endsWith("/-/merge_requests/new")) {
  369. setTimeout(() => {
  370. appendTemplateButton();
  371. appendAsTitleButton();
  372. }, 1e3);
  373. }
  374.  
  375. // src/monkey/gitlab-enhance/merge_requests/edit.ts
  376. if (/\/-\/merge_requests\/\d+\/edit$/.test(location.pathname)) {
  377. setTimeout(() => {
  378. appendTemplateButton();
  379. }, 1e3);
  380. }
  381. })();