GitHub - Pull Request - Compare Upstream Before Merging

This adds a Compare button next to the merge button which opens a new tab to compare upstream.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         GitHub - Pull Request - Compare Upstream Before Merging
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @description  This adds a Compare button next to the merge button which opens a new tab to compare upstream.
// @author       [email protected]
// @match        https://github.com/Audibene-GMBH/*/pull/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const PR_URL_REGEX = /https:\/\/github\.com\/(?<organizationId>[^/]+)\/(?<repo>[^/]+)\/pull\/(?<run>[^/]+)/;
    const COMPARE_WINDOW_NAME = '_compare';

    let compareUrl = null;
    let observer = null;

    function updateCompareUrlFromLocation() {
        const href = window.location.href;
        const match = href.match(PR_URL_REGEX);

        if (!match || !match.groups) {
            console.info(`CB4M: Not a supported PR URL, url=${href}`);
            compareUrl = null;
            return;
        }

        const { organizationId, repo } = match.groups;
        compareUrl = `https://github.com/${organizationId}/${repo}/compare/master...develop`;
        console.info(`CB4M: Compare URL hydrated: ${compareUrl}`);
    }

    function patchHistoryForLocationChange() {
        if (window.__cb4mHistoryPatched) return;
        window.__cb4mHistoryPatched = true;

        const { history } = window;
        if (!history || !history.pushState || !history.replaceState) return;

        const originalPushState = history.pushState.bind(history);
        const originalReplaceState = history.replaceState.bind(history);

        history.pushState = function pushStatePatched(...args) {
            const result = originalPushState(...args);
            window.dispatchEvent(new Event('pushstate'));
            window.dispatchEvent(new Event('locationchange'));
            return result;
        };

        history.replaceState = function replaceStatePatched(...args) {
            const result = originalReplaceState(...args);
            window.dispatchEvent(new Event('replacestate'));
            window.dispatchEvent(new Event('locationchange'));
            return result;
        };

        window.addEventListener('popstate', () => {
            window.dispatchEvent(new Event('locationchange'));
        });
    }

    function openCompareAndEnableMerge() {
        if (!compareUrl) {
            console.warn('CB4M: compareUrl is not set; aborting openCompareAndEnableMerge.');
            return;
        }

        window.open(compareUrl, COMPARE_WINDOW_NAME);

        const mergeButton = document.getElementById('merge_button');
        if (!mergeButton) {
            console.warn('CB4M: merge_button not found when trying to restore disabled state.');
            return;
        }

        const original = mergeButton.dataset.cb4mOriginalDisabled === '1';
        mergeButton.disabled = original;
        console.info(`CB4M: Merge button restored to original disabled=${original}`);
    }

    function isMergeButton(button) {
        if (!button) return false;
        if (typeof button.className !== 'string') return false;

        // Keep your original class check
        const hasExpectedClass = button.className.includes('prc-Button-ButtonBase');
        if (!hasExpectedClass) return false;

        const text = (button.textContent || '').toLowerCase().trim();

        // Handle "Rebase and merge", "Squash and merge", "Merge pull request", etc.
        if (!text.includes('merge')) return false;

        return true;
    }

    function findMergeButton() {
        // Narrow to button elements that look like the merge button
        const candidates = document.querySelectorAll('button.prc-Button-ButtonBase-c50BI, button.prc-Button-ButtonBase');

        for (const button of candidates) {
            if (isMergeButton(button)) {
                return button;
            }
        }

        return null;
    }

    function installCompareButton(mergeButton) {
        if (!mergeButton) {
            console.warn('CB4M: installCompareButton called without mergeButton.');
            return;
        }

        if (document.getElementById('compare_button')) {
            // Already installed
            return;
        }

        const container = mergeButton.parentElement;
        if (!container) {
            console.warn('CB4M: mergeButton has no parentElement.');
            return;
        }

        // Save original disabled state so we do not override real GitHub protections
        mergeButton.dataset.cb4mOriginalDisabled = mergeButton.disabled ? '1' : '0';

        // Create compare button as a fresh button element
        const compareButton = mergeButton.cloneNode();
        compareButton.id = 'compare_button';
        compareButton.type = 'button';
        compareButton.textContent = 'Compare';

        compareButton.disabled = false;
        compareButton.style.display = 'inline-block';
        compareButton.style.borderRadius = '.375rem';
        compareButton.style.marginRight = '8px';
        compareButton.style.backgroundColor = '#09910b';
        compareButton.style.color = '#fff';
        compareButton.style.verticalAlign = 'top';
        compareButton.style.cursor = 'pointer';

        compareButton.addEventListener('click', openCompareAndEnableMerge);

        // Now "lock" the merge button until compare is clicked
        mergeButton.disabled = true;
        mergeButton.id = 'merge_button';
        mergeButton.style.display = 'inline-block';
        mergeButton.style.verticalAlign = 'top';

        container.insertBefore(compareButton, mergeButton);

        console.info('CB4M: Compare button installed and merge button disabled.');
    }

    function handleDomChange() {
        if (!compareUrl) {
            // Not on a valid PR compare context
            return;
        }

        const mergeButton = findMergeButton();
        if (!mergeButton) return;

        installCompareButton(mergeButton);
    }

    function ensureObserver() {
        if (observer) return;

        observer = new MutationObserver(() => {
            try {
                handleDomChange();
            } catch (err) {
                console.error('CB4M: Error in MutationObserver callback', err);
            }
        });

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

        console.info('CB4M: MutationObserver attached.');
    }

    function initCompareBeforeMerge(caller) {
        console.info(`CB4M: Init from ${caller}`);

        window.c4bp = {
            ...(window.c4bp || {}),
            [caller]: true,
        };

        updateCompareUrlFromLocation();

        if (!compareUrl) {
            return;
        }

        ensureObserver();
        // Run once for already-rendered DOM
        handleDomChange();
    }

    patchHistoryForLocationChange();

    // Initial page load
    initCompareBeforeMerge('main');

    // Handle SPA-style navigation within GitHub
    window.addEventListener('locationchange', () => {
        initCompareBeforeMerge('locationchange');
    });
})();