// ==UserScript== // @name Phorge: copy commit reference // @namespace https://andrybak.dev // @author Andrei Rybak // @license AGPL-3.0-only // @version 1 // @description Adds a "Copy commit reference" button to every commit page on Phorge. // @homepageURL https://github.com/rybak/copy-commit-reference-userscript // @supportURL https://github.com/rybak/copy-commit-reference-userscript/issues // @match https://we.phorge.it/r* // @match https://we.phorge.it/R* // @match https://phabricator.wikimedia.org/r* // @match https://phabricator.wikimedia.org/R* // @icon https://we.phorge.it/file/data/qsmnldcb3vzxgaes3zge/PHID-FILE-jjurena7gu3ouojuoot7/favicon // @require https://cdn.jsdelivr.net/gh/rybak/userscript-libs@dc32d5897dcfa40a01c371c8ee0e211162dfd24c/waitForElement.js // @require https://cdn.jsdelivr.net/gh/rybak/copy-commit-reference-userscript@4df8332283727fd2650d5178e8b958b406fdd115/copy-commit-reference-lib.js // @grant none // @downloadURL none // ==/UserScript== /* * Copyright (C) 2024 Andrei Rybak * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ (function () { 'use strict'; const LOG_PREFIX = '[Phorge: copy commit reference]:'; function error(...toLog) { console.error(LOG_PREFIX, ...toLog); } function warn(...toLog) { console.warn(LOG_PREFIX, ...toLog); } function info(...toLog) { console.info(LOG_PREFIX, ...toLog); } function debug(...toLog) { console.debug(LOG_PREFIX, ...toLog); } /* * Implementation for Phorge. */ class Phorge extends GitHosting { /* * See PhabricatorDiffusionApplication.php for details. */ #hashRegex = new RegExp('[/](r(?[A-Z]+)|R(?[0-9]+):)(?[a-z0-9]+)$'); #unknownDate = 'unknown date'; /* * The `@match` entries cover a lot of URLs, so have to filter where the * userscript is active with overridden `isRecognized()`. */ isRecognized() { return document.location.pathname.match(this.#hashRegex) !== null; } getTargetSelector() { // sidebar on the right with "Download Raw Diff" return '.phabricator-action-list-view'; } getFullHash() { const matchPathname = document.location.pathname.match(this.#hashRegex); if (matchPathname === null) { error(`Cannot parse pathname: ${document.location.pathname}`); return null; } const selectorLetter = matchPathname.groups.repoCallSign ? 'r' : 'R'; const url = document.querySelector(`.phui-header-view .phui-header-subheader .phui-tag-view .phui-tag-core a[href^="/${selectorLetter}"]`)?.href; if (url === null) { error('Cannot find the self-linking URL'); return null; } const matchSelfLink = url.match(this.#hashRegex); if (matchSelfLink === null) { error(`Cannot parse URL: ${url}`); return null; } return matchSelfLink.groups.hash; } #monthNameToIso = { 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' }; #convertMonthNameToIso(s) { return this.#monthNameToIso[s]; } /* * It is hard to get a date out of Phorge/Phabricator commit view pages, * which don't have a timeline. * Example problematic pages: * - No timeline * https://phabricator.wikimedia.org/rMW34c64601bc37106cadfe7ea2f2d614da1deec48f * - Provenance "Authored on" without date * https://phabricator.wikimedia.org/rSVN105123 * - See also https://we.phorge.it/rPb02615bd5027ee51ac68d48a0a64306b75285789 * https://phabricator.wikimedia.org/rMWb1dea1d957833d1965999c350a05d365ba56adba */ getDateIso(hash) { const maybeTimelineDate = document.querySelector(`a[href$="${hash}"] ~ .phui-timeline-extra .print-only`)?.innerText?.slice(0, 'YYYY-MM-DD'.length); if (maybeTimelineDate) { // the good path, with machine-readable timestamp return maybeTimelineDate; } // oh no const provenanceDts = Array.from(document.querySelectorAll('.phui-property-list-key')).filter(dt => dt.innerText.includes('Provenance')); if (provenanceDts.length === 0) { return this.#unknownDate; } const provenanceAuthoredText = provenanceDts[0].nextSibling.querySelector('.phui-status-item-note')?.innerText if (provenanceAuthoredText === null) { return this.#unknownDate; } info('"Provenance" text:', provenanceAuthoredText); // Examples // Authored on Tue, Aug 27, 03:50 // Authored on Sep 30 2021, 16:41 const dateRegex = new RegExp(/Authored on ([A-Za-z]{3}, (?\w{3}) (?\d{1,2}).*|(?\w{3}) (?\d{1,2}) (?\d{4})), \d{2}:\d{2}/); const m = provenanceAuthoredText.match(dateRegex); if (m === null) { error('Cannot parse "Provenance":', provenanceAuthoredText); return this.#unknownDate; } if (m.groups.shortDay && m.groups.shortMonth) { const year = new Date().getFullYear(); const month = this.#convertMonthNameToIso(m.groups.shortMonth); return `${year}-${month}-${m.groups.shortDay}`; } else { const month = this.#convertMonthNameToIso(m.groups.longMonth); return `${m.groups.longYear}-${month}-${m.groups.longDay}`; } } getCommitMessage(hash) { return document.querySelector('.diffusion-commit-message').innerText; } wrapButtonContainer(innerContainer) { const li = document.createElement('li'); li.classList.add('phabricator-action-view', 'phabricator-action-view-submenu', 'phabricator-action-view-href', 'action-has-icon'); li.appendChild(innerContainer); return li; } wrapButton(button) { const actionItem = document.createElement('span'); actionItem.classList.add('phabricator-action-view-item'); const icon = document.createElement('span'); icon.classList.add('visual-only', 'phui-icon-view', 'phui-font-fa', 'fa-copy', 'phabricator-action-view-icon'); button.style.textDecoration = 'none'; button.style.color = '#464C5C'; button.style.padding = '4px 8px 6px 0'; actionItem.replaceChildren(icon, button); return actionItem; } createCheckmark() { const checkmark = super.createCheckmark(); checkmark.style.left = 'unset'; checkmark.style.right = 'calc(100% + 1.5rem)'; checkmark.style.zIndex = '100'; checkmark.style.top = '0.3rem'; return checkmark; } } CopyCommitReference.runForGitHostings(new Phorge()); })();