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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name Enhance some features of the Self-managed Gitlab
// @name:zh 强化自托管 Gitlab 能力
// @name:zh-CN 强化自托管 Gitlab 能力
// @name:zh-Hans 强化自托管 Gitlab 能力
// @name:zh-TW 強化自託管 Gitlab 能力
// @name:zh-HK 強化自託管 Gitlab 能力
// @name:zh-Hant 強化自託管 Gitlab 能力
// @namespace https://greatest.deepsurf.us/users/1133279
// @description Enhance some features of the Self-managed Gitlab, such as the CI/CD settings page, the merge request create/edit page etc.
// @description:zh 强化自托管 Gitlab 的一些功能,如 CI/CD 设置页面、合并请求创建/编辑页面等。
// @description:zh-CN 强化自托管 Gitlab 的一些功能,如 CI/CD 设置页面、合并请求创建/编辑页面等。
// @description:zh-Hans 强化自托管 Gitlab 的一些功能,如 CI/CD 设置页面、合并请求创建/编辑页面等。
// @description:zh-TW 強化自託管 Gitlab 的一些功能,如 CI/CD 設置頁面、合併請求創建/編輯頁面等。
// @description:zh-HK 強化自託管 Gitlab 的一些功能,如 CI/CD 設置頁面、合併請求創建/編輯頁面等。
// @description:zh-Hant 強化自託管 Gitlab 的一些功能,如 CI/CD 設置頁面、合併請求創建/編輯頁面等。
// @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGhlaWdodD0iNTEycHgiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDUxMiA1MTI7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEycHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxnIGlkPSJfeDMxXzQ0LWdpdGxhYiI+PGc+PGcgaWQ9IlhNTElEXzZfIj48Zz48Zz48cGF0aCBkPSJNNTIuNzUyLDIwNS41MDFsMjAzLjE4LDI2NC4wN2wtMjIyLjctMTY1LjI5Yy02LjExLTQuNTktOC43Mi0xMi41OC02LjM4LTE5Ljc3MWwyNS44Ny03OS4wNSAgICAgICBMNTIuNzUyLDIwNS41MDF6IiBzdHlsZT0iZmlsbDojRkNBMzI2OyIvPjwvZz48Zz48cG9seWdvbiBwb2ludHM9IjE3MS4zMDIsMjA1LjQ2MSAyNTYuMDEyLDQ2OS41NDEgMjU1LjkzMiw0NjkuNTcxIDUyLjc1MiwyMDUuNTAxIDUyLjgxMiwyMDUuNDYxICAgICAgICAgICAgICIgc3R5bGU9ImZpbGw6I0ZDNkQyNjsiLz48L2c+PGc+PHBvbHlnb24gcG9pbnRzPSIzNDAuNzMxLDIwNS40NjEgMjU2LjAyMSw0NjkuNTcxIDI1Ni4wMTIsNDY5LjU0MSAxNzEuMzAyLDIwNS40NjEgMTcxLjM5MiwyMDUuNDYxICAgICAgICAzNDAuNjQyLDIwNS40NjEgICAgICAiIHN0eWxlPSJmaWxsOiNFMjQzMjk7Ii8+PC9nPjxnPjxwb2x5Z29uIHBvaW50cz0iNDU5LjI5MiwyMDUuNTAxIDI1Ni4wMjEsNDY5LjU3MSAzNDAuNzMxLDIwNS40NjEgNDU5LjIzMSwyMDUuNDYxICAgICAgIiBzdHlsZT0iZmlsbDojRkM2RDI2OyIvPjwvZz48Zz48cGF0aCBkPSJNNDg1LjE5MSwyODQuNTExYzIuMjQsNy4xOS0wLjI3LDE1LjE4MS02LjQ3LDE5Ljc3MWwtMjIyLjcsMTY1LjI5bDIwMy4yNzEtMjY0LjA3bDAuMDI5LTAuMDQgICAgICAgTDQ4NS4xOTEsMjg0LjUxMXoiIHN0eWxlPSJmaWxsOiNGQ0EzMjY7Ii8+PC9nPjxnPjxwYXRoIGQ9Ik00MDguNDcyLDQ4LjQyMWw1MC43NiwxNTcuMDRoLTExOC41aC0wLjA5bDUwLjg1LTE1Ny4wNCAgICAgICBDMzk0LjM2MSw0MC40MzEsNDA1LjY4Miw0MC40MzEsNDA4LjQ3Miw0OC40MjF6IiBzdHlsZT0iZmlsbDojRTI0MzI5OyIvPjwvZz48Zz48cGF0aCBkPSJNMTcxLjM5MiwyMDUuNDYxaC0wLjA5SDUyLjgxMmw1MC43Ni0xNTcuMDRjMi44Ny03Ljk5LDE0LjE5LTcuOTksMTYuOTgsMCAgICAgICBDMTIwLjU1Miw0OC40MjEsMTcxLjMwMiwyMDUuNDYxLDE3MS4zOTIsMjA1LjQ2MXoiIHN0eWxlPSJmaWxsOiNFMjQzMjk7Ii8+PC9nPjwvZz48L2c+PC9nPjwvZz48ZyBpZD0iTGF5ZXJfMSIvPjwvc3ZnPg==
// @version 12
// @author Arylo
// @include /^https:\/\/(git(lab)?|code)\.[^/]+\/.*\/-\/settings\/ci_cd$/
// @include /^https:\/\/(git(lab)?|code)\.[^/]+\/.*\/-\/merge_requests\/new\b/
// @include /^https:\/\/(git(lab)?|code)\.[^/]+\/.*\/-\/merge_requests\/\d+/edit\b/
// @include /^https:\/\/(git(lab)?|code)\.[^/]+\/dashboard\/merge_requests\b/
// @license MIT
// @homepage https://greatest.deepsurf.us/zh-CN/scripts/519026
// @supportURL https://greatest.deepsurf.us/zh-CN/scripts/519026/feedback
// @run-at document-end
// @grant GM_addStyle
// @grant GM_setClipboard
// ==/UserScript==
"use strict";
(() => {
  // src/monkey/polyfill/GM.ts
  var thisGlobal = window;
  if (typeof thisGlobal.GM === "undefined") {
    thisGlobal.GM = {};
  }
  function getGMWindow() {
    return thisGlobal;
  }

  // src/monkey/polyfill/GM_addStyle.ts
  var w = getGMWindow();
  if (typeof w.GM_addStyle === "undefined") {
    w.GM_addStyle = function GM_addStyle2(cssContent) {
      const head = document.getElementsByTagName("head")[0];
      if (head) {
        const styleElement = document.createElement("style");
        styleElement.setAttribute("type", "text/css");
        styleElement.textContent = cssContent;
        head.appendChild(styleElement);
        return styleElement;
      }
      return null;
    };
  }
  if (typeof w.GM.addStyle === "undefined") {
    w.GM.addStyle = GM_addStyle;
  }

  // src/monkey/gitlab-enhance/settings/ci_cd.css
  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";

  // src/monkey/gitlab-enhance/settings/ci_cd.ts
  if (location.pathname.endsWith("/-/settings/ci_cd")) {
    setTimeout(() => GM_addStyle(ci_cd_default), 25);
  }

  // src/monkey/gitlab-enhance/dashboard/merge_requests.ts
  var hyperlinkResource = () => {
    const mergeRequests = $(".merge-request:not([hyperlinked])");
    mergeRequests.each((_, mergeRequestEle) => {
      const href = $(".js-prefetch-document", mergeRequestEle).attr("href");
      if (!href) return;
      const resourceUrl = href.replace(/\/-\/merge_requests\/\d+$/, "");
      const rawRefEle = $(".issuable-reference", mergeRequestEle);
      const rawRefName = rawRefEle.text();
      const [resourceName, number] = rawRefName.split("!");
      rawRefEle.html(`<a href="${resourceUrl}">${resourceName}</a>!${number}`);
      $(mergeRequestEle).attr("hyperlinked", "");
    });
  };
  if (location.pathname.endsWith("/dashboard/merge_requests")) {
    $(".issuable-list").on("mouseenter", hyperlinkResource);
    setTimeout(hyperlinkResource, 1e3);
  }

  // src/monkey/gitlab-enhance/utils.ts
  var getButtonElement = (text2) => {
    const classnames = "gl-font-sm! gl-ml-3 gl-button btn btn-default btn-sm";
    return $(`<a class="${classnames}">${text2}</a>`);
  };

  // packages/MdGenerator/MdTools.ts
  var INDENT_SPACE_LENGTH = 2;
  function indent(level, content = "") {
    return `${Array((level - 1) * INDENT_SPACE_LENGTH).fill(" ").join("")}${content}`;
  }
  function enter() {
    return "\n";
  }
  var header = (level) => (text2 = "") => `${Array(level).fill("#").join("")} ${text2}`.trim();
  function h1(...args) {
    return header(1)(...args);
  }
  function h2(...args) {
    return header(2)(...args);
  }
  function h3(...args) {
    return header(3)(...args);
  }
  function h4(...args) {
    return header(4)(...args);
  }
  function h5(...args) {
    return header(5)(...args);
  }
  function text(content = "") {
    return content;
  }
  function anchor(key, url) {
    return `[${key}]: ${url}`;
  }
  function hyperlink(label, url) {
    return `[${label}](${url})`;
  }
  function hyperlinkWithKey(label, key) {
    return `[${label}][${key}]`;
  }
  function image(url, alt = "") {
    return `!${hyperlink(alt, url)}`;
  }
  function imageByKey(key, alt = "") {
    return `!${hyperlinkWithKey(alt, key)}`;
  }
  function listItem(text2 = "") {
    return `- ${text2}`.trimEnd();
  }
  function taskItem(text2 = "", { selected = false } = {}) {
    return `[${selected ? "x" : " "}] ${text2}`.trimEnd();
  }

  // packages/MdGenerator/index.ts
  function getOption(options, key, defaultValue) {
    return options[key] ?? defaultValue;
  }
  function getLevelFromOptions(options = {}) {
    const { level = 1 } = options;
    return level;
  }
  function cloneDeep(value) {
    return JSON.parse(JSON.stringify(value));
  }
  var Template = class {
    constructor(text2) {
      this.anchorMap = {};
      this.templateContent = text2;
      this.headerNextUtils = {
        text: this.text.bind(this),
        image: this.image.bind(this),
        hyperlink: this.hyperlink.bind(this),
        listItem: this.listItem.bind(this),
        taskItem: this.taskItem.bind(this)
      };
      this.contentNextUtils = {
        ...this.headerNextUtils,
        end: this.emptyLine.bind(this)
      };
    }
    h1(text2) {
      this.text(h1(text2));
      this.emptyLine();
      return this.headerNextUtils;
    }
    h2(text2) {
      this.text(h2(text2));
      this.emptyLine();
      return this.headerNextUtils;
    }
    h3(text2) {
      this.text(h3(text2));
      this.emptyLine();
      return this.headerNextUtils;
    }
    h4(text2) {
      this.text(h4(text2));
      this.emptyLine();
      return this.headerNextUtils;
    }
    h5(text2) {
      this.text(h5(text2));
      this.emptyLine();
      return this.headerNextUtils;
    }
    [Symbol.toStringTag]() {
      return [
        this.templateContent,
        this.templateContent && !/\n$/.test(this.templateContent) && enter(),
        Object.keys(this.anchorMap).length && enter(),
        Object.entries(this.anchorMap).map(([key, value]) => anchor(key, value)).join(enter()),
        Object.keys(this.anchorMap).length && enter()
      ].filter(Boolean).join("").replace(/\n(\s*\n){2,}/g, "\n\n");
    }
    text(content = "", opts) {
      this.templateContent += indent(getLevelFromOptions(opts), `${text(content)}${enter()}`);
      return this.contentNextUtils;
    }
    listItem(text2 = "", opts) {
      return this.text(listItem(text2), { level: getLevelFromOptions(opts) });
    }
    taskItem(text2 = "", opts) {
      return this.listItem(taskItem(text2, { selected: getOption(opts || {}, "selected", false) }), { level: getLevelFromOptions(opts) });
    }
    hyperlink(text2, link, opts) {
      let content;
      const anchorKey = getOption(opts || {}, "anchorKey", "");
      if (typeof anchorKey === "string" && anchorKey.length) {
        anchor(anchorKey, link);
        content = hyperlinkWithKey(text2, anchorKey);
      } else {
        content = hyperlink(text2, link);
      }
      return this.text(content, { level: getLevelFromOptions(opts) });
    }
    image(url, opts) {
      let content;
      const anchorKey = getOption(opts || {}, "anchorKey", "");
      const alt = getOption(opts || {}, "alt", "");
      if (typeof anchorKey === "string" && anchorKey.length) {
        this.anchor(anchorKey, url);
        content = imageByKey(alt, anchorKey);
      } else {
        content = image(alt, url);
      }
      return this.text(content, { level: getLevelFromOptions(opts) });
    }
    table(opts) {
      const tableMap = { header: [], body: [] };
      const actionMap = {
        header: (row) => {
          row.forEach(({ key, title }) => {
            tableMap.header.push({ key, title });
          });
          return actionMap;
        },
        body: (row) => {
          tableMap.body.push(row);
          return actionMap;
        },
        end: () => {
          if (!tableMap.header.length) return "";
          const { header: header2, body } = tableMap;
          const headerContent = header2.map(({ title }) => `|${title}`).join("") + "|";
          this.text(headerContent, { level: getLevelFromOptions(opts) });
          const separator = header2.map(() => "|--").join("") + "|";
          this.text(separator, { level: getLevelFromOptions(opts) });
          body.forEach((row) => {
            const content = header2.map(({ key }) => `|${(row[key] ?? "").toString().replace(/\|/g, "|")}`).join("") + "|";
            this.text(content, { level: getLevelFromOptions(opts) });
          });
        }
      };
      return actionMap;
    }
    anchor(key, link) {
      this.anchorMap[key] = link;
    }
    emptyLine() {
      if (!this.templateContent.endsWith(enter())) {
        this.templateContent += enter();
      }
      this.templateContent += enter();
    }
    modify(callback) {
      this.templateContent = callback(cloneDeep(this.templateContent));
      return this.contentNextUtils;
    }
  };
  var genTemplate = (callback = () => {
  }) => {
    return readTemplate("", callback);
  };
  var readTemplate = (text2, callback = () => {
  }) => {
    const templateInst = new Template(cloneDeep(text2));
    callback(templateInst);
    return templateInst[Symbol.toStringTag]();
  };

  // src/monkey/gitlab-enhance/merge_requests/template.css
  var template_default = ".gl-display-flex:has([for=merge_request_description]){align-items:baseline}\n";

  // src/monkey/gitlab-enhance/merge_requests/template.ts
  var getBranchType = () => {
    const fromBranchName = $(".align-self-center code:not([data-branch-name])").text();
    const prefixBranchName = fromBranchName.split("/")[0].toLowerCase();
    switch (prefixBranchName) {
      case "feature":
      case "feat":
        return 0 /* FEATURE */;
      case "fix":
      case "bugfix":
        return 1 /* BUGFIX */;
      case "hotfix":
        return 2 /* HOTFIX */;
      case "devops":
      case "chore":
      case "test":
      case "doc":
      case "docs":
        return 3 /* TASKS */;
      default:
        return 4 /* OTHERS */;
    }
  };
  var generateTemplate = () => {
    const branchType = getBranchType();
    return genTemplate((utils) => {
      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();
      utils.h2("Description");
      if (branchType !== 0 /* FEATURE */) {
        utils.h3("Why (Why does this happen?)").listItem().end();
        utils.h3("How (How can we avoid or solve it?)").listItem().end();
      }
      utils.h3("What (What did you do this time?)").listItem().end();
      if (branchType !== 3 /* TASKS */) {
        utils.h3("Results (Screenshot, etc)");
        utils.h4("Before modification");
        utils.h4("After modification");
        utils.h2("Affected Zone").listItem("Affected Module(s):").listItem("Affected URL(s):").end();
      }
      utils.h2("External resources (Mention, Resolves, or Closes)");
    });
  };
  var appendTemplateButton = () => {
    GM_addStyle(template_default);
    const text2 = "Copy Template";
    const btnElement = getButtonElement(text2);
    const templateContent = generateTemplate();
    const hint = $('<span class="gl-font-sm! gl-ml-3 gl-text-secondary"></span>');
    let setTimeoutId;
    btnElement.on("click", async () => {
      hint.remove();
      setTimeoutId && clearTimeout(setTimeoutId);
      hint.text("Copying...");
      btnElement.after(hint);
      await GM_setClipboard(templateContent, "text", () => {
        hint.text("Copied!");
        setTimeoutId = setTimeout(() => hint.remove(), 3e3);
      });
    });
    $("*:has(>[for=merge_request_description])").append(btnElement);
  };

  // src/monkey/gitlab-enhance/merge_requests/new.ts
  var appendAsTitleButton = () => {
    $(".commit-content").each((_, el) => {
      const titleElements = $(".item-title", el);
      const title = titleElements.text();
      const btnElement = getButtonElement("As title");
      btnElement.on("click", () => {
        $("input[data-testid=issuable-form-title-field]").val(title);
        $("input[data-testid=issuable-form-title-field]").focus();
      });
      $(".committer", el).before(btnElement);
    });
  };
  if (location.pathname.endsWith("/-/merge_requests/new")) {
    setTimeout(() => {
      appendTemplateButton();
      appendAsTitleButton();
    }, 1e3);
  }

  // src/monkey/gitlab-enhance/merge_requests/edit.ts
  if (/\/-\/merge_requests\/\d+\/edit$/.test(location.pathname)) {
    setTimeout(() => {
      appendTemplateButton();
    }, 1e3);
  }
})();