Graphite GitHub button

Add a button to go from app.graphite.com to github.com

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @author      [email protected]
// @name        Graphite GitHub button
// @description Add a button to go from app.graphite.com to github.com
// @match       https://app.graphite.dev/*
// @match       https://app.graphite.com/*
// @version      0.5.1
// @run-at      document-start
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @license      MIT
// @namespace https://app.graphite.dev
// ==/UserScript==

const PATH_REGEX = /^\/github\/pr\/([\w-]+)\/([\w-]+)\/(\d+).*$/;
const SELECTOR =
  '[class^="PullRequestTitleBar_container_"] > div:nth-child(1) > div:nth-child(2)';

/**
 * Finds the "Review changes" button to use as a style template,
 * then creates and appends a new "Open in GitHub" button
 * that matches its appearance.
 *
 * Assumes PATH_REGEX is defined in the script's outer scope.
 *
 * @param {HTMLElement} toolbar - The toolbar element to append the button to.
 */
const addButton = (toolbar) => {
  // --- 1. Get PR info ---
  const [_, org, repo, pr] = window.location.pathname.match(PATH_REGEX);
  const gitHubLink = `https://github.com/${org}/${repo}/pull/${pr}`;

  // --- 2. Check if button already exists ---
  if (document.getElementById("gitHubLink") != null) {
    return;
  }

  // --- 3. Find the "Review changes" button to use as a template ---
  const reviewSpan = document.evaluate(
    "//span[normalize-space()='Review changes']", // Use XPath to find the text node
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;

  if (!reviewSpan) {
    console.error(
      'Tampermonkey Script: Could not find "Review changes" span to clone styles.'
    );
    return;
  }

  const templateButton = reviewSpan.closest("button");

  if (!templateButton) {
    console.error(
      'Tampermonkey Script: Could not find parent "Review changes" button to clone styles.'
    );
    return;
  }

  // --- 4. Get the class names from the template button's inner spans ---
  // We need to replicate the <span class="..."><span class="..."><span...
  const templateInnerSpan = templateButton.querySelector("span");
  const templateTextSpan = templateInnerSpan
    ? templateInnerSpan.querySelector("span")
    : null;

  if (!templateInnerSpan || !templateTextSpan) {
    console.error(
      "Tampermonkey Script: Could not find inner span structure of template button."
    );
    return;
  }

  // --- 5. Create the new "Open in GitHub" link ---
  const anchorEl = document.createElement("a");
  anchorEl.setAttribute("id", "gitHubLink");
  anchorEl.setAttribute("href", gitHubLink);

  // --- 6. Copy all classes and data attributes from the template button ---
  anchorEl.className = templateButton.className;
  for (const attr of templateButton.attributes) {
    if (attr.name.startsWith("data-")) {
      anchorEl.setAttribute(attr.name, attr.value);
    }
  }
  // Ensure it's treated as a button role for accessibility, though it's a link
  anchorEl.setAttribute("role", "button");

  // --- 7. Re-create the inner span structure for correct styling ---
  const span1 = document.createElement("span");
  span1.className = templateInnerSpan.className; // e.g., "Button_gdsButtonContents__5B2fy"

  const span2 = document.createElement("span");
  span2.className = templateTextSpan.className; // e.g., "Button_gdsButtonText__5kyh_"

  const span3 = document.createElement("span");
  span3.appendChild(document.createTextNode("Open in GitHub"));

  span2.appendChild(span3);
  span1.appendChild(span2);
  anchorEl.appendChild(span1);

  // --- 8. Append the new button to the toolbar ---
  toolbar.appendChild(anchorEl);
};

const toolbarObserver = new MutationObserver((_, observer) => {
  const toolbar = document.querySelector(SELECTOR);
  if (toolbar) {
    observer.disconnect();
    addButton(toolbar);
  }
});

let lastPathname;
const routeChangeObserver = new MutationObserver(() => {
  const { pathname } = window.location;

  if (pathname !== lastPathname) {
    lastPathname = pathname;

    if (pathname.match(PATH_REGEX)) {
      toolbarObserver.observe(document.body, {
        childList: true,
        subtree: true,
      });
    }
  }
});

routeChangeObserver.observe(document.body, { childList: true, subtree: true });