// ==UserScript== // @name Code Helpers: Graphite — Open in GitHub // @namespace https://github.com/NorthIsUp/userscripts/graphite-open-in-github // @version 1.0.5 // @description Adds an "Open in GitHub" button to the current PR row in Graphite's stack view. // @icon data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2248%22%20height=%2248%22%20viewBox=%220%200%2028%2028%22%3E%3Cstyle%3E:root%7B--fill:%23000%7D@media%20(prefers-color-scheme:dark)%7B:root%7B--fill:%23fff%7D%7D%3C/style%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22m20.704%207.123-9.27-2.484-6.788%206.793%202.482%209.276%209.27%202.484%206.788-6.793-2.482-9.276Z%22%3E%3C/path%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22M17.644%200%203.73%203.729%200%2017.644l10.187%2010.187%2013.915-3.729%203.73-13.915L17.643%200Zm2.27%2024.312H7.917L1.92%2013.915%207.917%203.518h11.997l5.998%2010.397-5.998%2010.397Z%22%3E%3C/path%3E%3C/svg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2264%22%20height=%2264%22%20viewBox=%220%200%2028%2028%22%3E%3Cstyle%3E:root%7B--fill:%23000%7D@media%20(prefers-color-scheme:dark)%7B:root%7B--fill:%23fff%7D%7D%3C/style%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22m20.704%207.123-9.27-2.484-6.788%206.793%202.482%209.276%209.27%202.484%206.788-6.793-2.482-9.276Z%22%3E%3C/path%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22M17.644%200%203.73%203.729%200%2017.644l10.187%2010.187%2013.915-3.729%203.73-13.915L17.643%200Zm2.27%2024.312H7.917L1.92%2013.915%207.917%203.518h11.997l5.998%2010.397-5.998%2010.397Z%22%3E%3C/path%3E%3C/svg%3E // @match https://app.graphite.com/* // @match https://github.com/NorthIsUp/userscripts/releases* // @run-at document-end // @grant none // @updateURL https://raw.githubusercontent.com/NorthIsUp/userscripts/dist/graphite-open-in-github.user.js // @downloadURL https://raw.githubusercontent.com/NorthIsUp/userscripts/dist/graphite-open-in-github.user.js // ==/UserScript== (function () { 'use strict'; // Installed-script beacon — src/lib/beacon.ts, inlined here so it runs // before anything else: announce this script on https://github.com/NorthIsUp/userscripts/releases, // then stop unless this script's own @match covers the page. { /** The attribute the tags carry, and what a reader queries for. */ const BEACON_TAG = 'userscript-beacon'; /** * On the beacon page, leave a `` carrying this * script's identity. Returns whether the script should go on running here — * false on the beacon page when the script's own `@match` doesn't cover it. * * @param script who this is: the header's name, namespace and version. * @param pageRe source of a RegExp matching the page to announce on. * @param ownRes sources of RegExps for the script's own `@match` patterns. */ function announce(script, pageRe, ownRes) { // A fragment is never part of a @match, and never changes which page this is. const here = location.href.split('#')[0]; if (new RegExp(pageRe).test(here)) { const tag = document.createElement('meta'); tag.name = BEACON_TAG; Object.assign(tag.dataset, script); // document-start runs before exists, so mount whenever it does. const mount = () => (document.head || document.documentElement).appendChild(tag); if (document.head || document.documentElement) mount(); else document.addEventListener('DOMContentLoaded', mount, { once: true }); } return ownRes.some((re) => new RegExp(re).test(here)); } if (!announce({"name":"Code Helpers: Graphite — Open in GitHub","namespace":"https://github.com/NorthIsUp/userscripts/graphite-open-in-github","version":"1.0.5"}, "^https:\\/\\/github\\.com\\/NorthIsUp\\/userscripts\\/releases.*$", ["^https:\\/\\/app\\.graphite\\.com\\/.*$"])) return; } /** Run `fn` now and after any DOM change, coalesced to one call per frame. */ function observeDom(fn, root = document.documentElement) { let queued = false; const tick = () => { if (queued) return; queued = true; requestAnimationFrame(() => { queued = false; fn(); }); }; fn(); new MutationObserver(tick).observe(root, { childList: true, subtree: true }); } const MARKER = 'data-github-link'; // GitHub mark, viewBox 0 0 48 48 (same artwork as the script's @icon). const GH_PATH = 'M24 1.9a21.6 21.6 0 0 0-6.8 42.2c1 .2 1.8-.9 1.8-1.8v-2.9c-6 1.3-7.9-2.9-7.9-2.9a6.5 6.5 0 0 0-2.2-3.2c-2-1.4.1-1.3.1-1.3a4.3 4.3 0 0 1 3.3 2c1.7 2.9 5.5 2.6 6.7 2.1a5.4 5.4 0 0 1 .5-2.9C12.7 32 9 28 9 22.6a10.7 10.7 0 0 1 2.9-7.6 6.2 6.2 0 0 1 .3-6.4 8.9 8.9 0 0 1 6.4 2.9 15.1 15.1 0 0 1 5.4-.8 17.1 17.1 0 0 1 5.4.7 9 9 0 0 1 6.4-2.8 6.5 6.5 0 0 1 .4 6.4 10.7 10.7 0 0 1 2.8 7.6c0 5.4-3.7 9.4-10.5 10.6a5.4 5.4 0 0 1 .5 2.9v6.2a1.8 1.8 0 0 0 1.9 1.8A21.7 21.7 0 0 0 24 1.9Z'; function githubUrl(graphitePath) { const m = graphitePath.match(/\/github\/pr\/([^/]+)\/([^/]+)\/(\d+)/); return m && `https://github.com/${m[1]}/${m[2]}/pull/${m[3]}`; } function decorate() { const row = document.querySelector('a[aria-current="true"][href*="/github/pr/"]'); if (!row) return; const url = githubUrl(row.getAttribute('href') || ''); if (!url) return; // Graphite's action slot lives inside the row ; nest a