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