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, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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