GitHub PR Copy Diff

Adds a "Copy Diff" button to the PR nav that copies the unified diff to the clipboard

目前為 2026-06-04 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==UserScript==
// @name         GitHub PR Copy Diff
// @namespace    http://tampermonkey.net/
// @icon         https://github.githubassets.com/favicons/favicon-dark.png
// @version      2026.06.04.1
// @description  Adds a "Copy Diff" button to the PR nav that copies the unified diff to the clipboard
// @author       KakkoiDev
// @match        https://github.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @connect      github.com
// @connect      githubusercontent.com
// @license      MIT
// ==/UserScript==

// Broad @match needed: GitHub uses SPA navigation, so the MutationObserver
// re-adds the button as the PR header re-renders.
//
// GM_xmlhttpRequest + GM_setClipboard (not fetch + navigator.clipboard):
// github.com/<o>/<r>/pull/<n>.diff 302-redirects cross-origin to
// patch-diff.githubusercontent.com with no CORS header, so a page fetch is
// blocked. GM_xmlhttpRequest bypasses CORS and sends cookies (private PRs).

(function() {
    'use strict';

    function getPrInfo() {
        const m = location.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
        if (!m) return null;
        return { owner: m[1], repo: m[2], number: m[3] };
    }

    function flash(btn, msg) {
        btn.textContent = msg;
        btn.disabled = false;
        setTimeout(() => { btn.textContent = 'Copy Diff'; }, 1500);
    }

    function copyDiff(btn) {
        const pr = getPrInfo();
        if (!pr) return;
        const diffUrl = `${location.origin}/${pr.owner}/${pr.repo}/pull/${pr.number}.diff`;
        btn.disabled = true;
        btn.textContent = 'Copying...';
        GM_xmlhttpRequest({
            method: 'GET',
            url: diffUrl,
            onload: (res) => {
                if (res.status >= 200 && res.status < 300) {
                    GM_setClipboard(res.responseText, 'text');
                    flash(btn, 'Copied!');
                } else {
                    flash(btn, 'Failed');
                }
            },
            onerror: () => flash(btn, 'Failed'),
        });
    }

    function addCopyDiffButton() {
        const pr = getPrInfo();
        if (!pr) return;

        const filesTab = document.querySelector('#prs-files-anchor-tab')
            || document.querySelector(`a[href$="/pull/${pr.number}/files"]`);
        if (!filesTab) return;
        if (document.getElementById('gh-copy-diff-btn')) return;

        const btn = document.createElement('button');
        btn.id = 'gh-copy-diff-btn';
        btn.type = 'button';
        btn.className = 'btn btn-sm';
        btn.textContent = 'Copy Diff';
        btn.style.marginLeft = '8px';
        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            copyDiff(btn);
        });

        filesTab.insertAdjacentElement('afterend', btn);
    }

    addCopyDiffButton();

    const observer = new MutationObserver(() => {
        addCopyDiffButton();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();