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