強化自託管 Gitlab 能力

強化自託管 Gitlab 的一些功能,如 CI/CD 設置頁面、合併請求創建/編輯頁面等。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
  }
})();