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.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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