Graphite GitHub button

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

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 or Violentmonkey 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.

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

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.

ستحتاج إلى تثبيت إضافة مثل 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 });