您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
This script is obsolete. Please migrate to userscript "Git: copy commit reference"
当前为
// ==UserScript== // @name GitHub: Copy Commit Reference // @namespace https://github.com/rybak // @license MIT // @version 4-alpha // @description This script is obsolete. Please migrate to userscript "Git: copy commit reference" // @author Andrei Rybak // @include https://*github*/*/commit/* // @match https://github.example.com/*/commit/* // @match https://github.com/*/commit/* // @icon https://github.githubassets.com/favicons/favicon-dark.png // @grant none // ==/UserScript== /* * Copyright (c) 2023 Andrei Rybak * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* * FIXME known issues; * - On-the-fly loading from non-commit pages */ (function() { 'use strict'; const LOG_PREFIX = '[GitHub: copy commit reference]:'; const CONTAINER_ID = "GHCCR_container"; const CHECKMARK_ID = "GHCCR_checkmark"; let inProgress = false; 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); } /* * Extracts the first line of the commit message. * If the first line is too small, extracts more lines. */ function commitMessageToSubject(commitMessage) { const lines = commitMessage.split('\n'); if (lines[0].length > 16) { /* * Most common use-case: a normal commit message with * a normal-ish subject line. */ return lines[0].trim(); } /* * The `if`s below handle weird commit messages I have * encountered in the wild. */ if (lines.length < 2) { return lines[0].trim(); } if (lines[1].length == 0) { return lines[0].trim(); } // sometimes subject is weirdly split across two lines return lines[0].trim() + " " + lines[1].trim(); } function abbreviateCommitId(commitId) { return commitId.slice(0, 7) } /* * Formats given commit metadata as a commit reference according * to `git log --format=reference`. See format descriptions at * https://git-scm.com/docs/git-log#_pretty_formats */ function plainTextCommitReference(commitId, subject, dateIso) { debug(`plainTextCommitReference("${commitId}", "${subject}", "${dateIso}")`); const abbrev = abbreviateCommitId(commitId); return `${abbrev} (${subject}, ${dateIso})`; } /* * Inserts an HTML anchor to link to the pull requests, which are * mentioned in the provided `text` in the format that is used by * GitHub's default automatic merge commit messages. */ async function insertPrLinks(text, commitId) { if (!text.toLowerCase().includes('pull request')) { return text; } try { // a hack: just get the existing HTML from the GUI // the hack probably doesn't work very well with overly long subject lines // TODO: proper conversion of `text` // though a shorted version (with ellipsis) might be better for HTML version return document.querySelector('.commit-title.markdown-title').innerHTML.trim(); } catch (e) { error("Cannot insert pull request links", e); return text; } } /* * Renders given commit that has the provided subject line and date * in reference format as HTML content, which includes a clickable * link to the commit. * * Documentation of formats: https://git-scm.com/docs/git-log#_pretty_formats */ async function htmlSyntaxLink(commitId, subject, dateIso) { const url = document.location.href; const abbrev = abbreviateCommitId(commitId); let subjectHtml; subjectHtml = await insertPrLinks(subject, commitId); debug("subjectHtml", subjectHtml); const html = `<a href="${url}">${abbrev}</a> (${subjectHtml}, ${dateIso})`; return html; } function addLinkToClipboard(event, plainText, html) { event.stopPropagation(); event.preventDefault(); let clipboardData = event.clipboardData || window.clipboardData; clipboardData.setData('text/plain', plainText); clipboardData.setData('text/html', html); } function getApiHostUrl() { const host = document.location.host; return `https://api.${host}`; } function getFullCommitId() { const path = document.querySelector('a.js-permalink-shortcut').getAttribute('href'); const parts = path.split('/'); if (parts.length < 5) { throw new Error("Cannot find commit hash in the URL"); } const commitId = parts[4]; return commitId; } function getCommitRestApiUrl(commitId) { // /repos/{owner}/{repo}/commits/{ref} // e.g. https://api.github.com/repos/rybak/atlassian-tweaks/commits/a76a9a6e993a7a0e48efabdd36f4c893317f1387 // NOTE: plural "commits" in the URL!!! const apiHostUrl = getApiHostUrl(); const path = document.querySelector('a.js-permalink-shortcut').getAttribute('href'); const parts = path.split('/'); if (parts.length < 5) { throw new Error("Cannot find commit hash in the URL"); } const owner = parts[1]; const repo = parts[2]; return `${apiHostUrl}/repos/${owner}/${repo}/commits/${commitId}`; } function getRestApiOptions() { const myHeaders = new Headers(); myHeaders.append("Accept", "application/vnd.github+json"); const myInit = { headers: myHeaders, }; return myInit; } /* * Generates the content and passes it to the clipboard. * * Async, because we need to access REST API. */ async function copyClickAction(event) { event.preventDefault(); try { /* * Extract metadata about the commit from the UI. */ let commitJson; const commitId = getFullCommitId(); try { const commitRestUrl = getCommitRestApiUrl(commitId); info(`Fetching "${commitRestUrl}"...`); const commitResponse = await fetch(commitRestUrl, getRestApiOptions()); commitJson = await commitResponse.json(); } catch (e) { error("Cannot fetch commit JSON from REST API", e); } /* * If loaded successfully, extract particular parts of * the JSON that we are interested in. */ const dateIso = commitJson.commit.author.date.slice(0, 'YYYY-MM-DD'.length); const commitMessage = commitJson.commit.message; const subject = commitMessageToSubject(commitMessage); const plainText = plainTextCommitReference(commitId, subject, dateIso); const html = await htmlSyntaxLink(commitId, subject, dateIso); info("plain text:", plainText); info("HTML:", html); const handleCopyEvent = e => { addLinkToClipboard(e, plainText, html); }; document.addEventListener('copy', handleCopyEvent); document.execCommand('copy'); document.removeEventListener('copy', handleCopyEvent); } catch (e) { error('Could not do the copying', e); } } // from https://stackoverflow.com/a/61511955/1083697 by Yong Wang function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } // adapted from https://stackoverflow.com/a/35385518/1083697 by Mark Amery function htmlToElement(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.firstChild; } function showCheckmark() { const checkmark = document.getElementById(CHECKMARK_ID); checkmark.style.display = 'inline'; } function hideCheckmark() { const checkmark = document.getElementById(CHECKMARK_ID); checkmark.style.display = 'none'; } function createCopyLink() { const onclick = (event) => { showCheckmark(); copyClickAction(event); setTimeout(hideCheckmark, 2000); } const linkText = "Copy commit reference"; const style = 'margin-left: 1em;'; const anchor = htmlToElement(`<a href="#" style="${style}" class="Link--onHover color-fg-muted"></a>`); const icon = document.querySelector('.octicon-copy').cloneNode(true); icon.classList.remove('color-fg-muted'); anchor.append(icon); anchor.append(` ${linkText}`); anchor.onclick = onclick; return anchor; } function createCheckmark() { const container = document.createElement('span'); container.id = CHECKMARK_ID; container.style.display = 'none'; container.innerHTML = " ✅ Copied!"; return container; } function doAddLink() { waitForElement('.commit.full-commit .commit-meta div.flex-self-start.flex-content-center').then(target => { debug('target', target); const container = htmlToElement(`<span id="${CONTAINER_ID}"></span>`); if (document.getElementById(CONTAINER_ID) != null) { info("OOPS"); return; } target.append(container); const link = createCopyLink(); container.append(' '); container.appendChild(link); container.append(createCheckmark()); }); } function removeExistingContainer() { const container = document.getElementById(CONTAINER_ID); if (!container) { return; } container.parentNode.removeChild(container); } function ensureLink() { if (inProgress) { return; } inProgress = true; try { removeExistingContainer(); /* * Need this tag to have parent for the container. */ waitForElement('.commit.full-commit .commit-meta').then(loadedBody => { doAddLink(); if (document.getElementById(CONTAINER_ID) == null) { ensureLink(); } }); } catch (e) { error('Could not create the button', e); } finally { inProgress = false; } } ensureLink(); /* * Handling of on-the-fly page loading. * * - The usual MutationObserver on <title> doesn't work. * - None of the below event listeners work: * - https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event * - https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event * - https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event * * I found 'soft-nav:progress-bar:start' in a call stack in GitHub's own JS, * and just tried replacing "start" with "end". So far, seems to work fine. */ document.addEventListener('soft-nav:progress-bar:end', (event) => { info("progress-bar:end", event); ensureLink(); }); })();