import './wait-for-checks.css'; import React from 'dom-chef'; import select from 'select-dom'; import onetime from 'onetime'; import {InfoIcon} from '@primer/octicons-react'; import * as pageDetect from 'github-url-detection'; import pRetry, {AbortError} from 'p-retry'; import delegate, {DelegateEvent} from 'delegate-it'; import features from '../feature-manager.js'; import observeElement from '../helpers/simplified-element-observer.js'; import * as prCiStatus from '../github-helpers/pr-ci-status.js'; import onPrMergePanelOpen from '../github-events/on-pr-merge-panel-open.js'; import {onPrMergePanelLoad} from '../github-events/on-fragment-load.js'; import onAbort from '../helpers/abort-controller.js'; import {userCanLikelyMergePR} from '../github-helpers/index.js'; import {isHasSelectorSupported} from '../helpers/select-has.js'; import {actionsTab, prCommitStatusIcon} from '../github-helpers/selectors.js'; import observe from '../helpers/selector-observer.js'; // Reuse the same checkbox to preserve its state const generateCheckbox = onetime(() => ( )); function getCheckbox(): HTMLInputElement | undefined { return select('input[name="rgh-pr-check-waiter"]'); } // Only show the checkbox if the last commit doesn't have a green or red CI icon function showCheckboxIfNecessary(): void { const checkbox = getCheckbox(); const lastCommitStatus = prCiStatus.getLastCommitStatus(); const isNecessary = lastCommitStatus === prCiStatus.PENDING // If the latest commit is missing an icon, add the checkbox as long as there's at least one CI icon on the page (including `ci-link`) || (lastCommitStatus === false && select.exists(prCommitStatusIcon)); if (!checkbox && isNecessary) { select('.js-merge-form .select-menu')?.append(generateCheckbox()); } else if (checkbox && !isNecessary) { checkbox.parentElement!.remove(); } } let waiting: symbol | undefined; function disableForm(disabled = true): void { for (const field of select.all(` textarea[name="commit_message"], input[name="commit_title"], input[name="rgh-pr-check-waiter"], button.js-merge-commit-button `)) { field.disabled = disabled; } // Enabled form = no waiting in progress if (!disabled) { waiting = undefined; } } async function handleMergeConfirmation(event: DelegateEvent): Promise { if (!getCheckbox()?.checked) { return; } const lastCommitSha = prCiStatus.getLastCommitReference()?.trim(); if (!lastCommitSha) { return; } event.preventDefault(); disableForm(); const currentConfirmation = Symbol(''); waiting = currentConfirmation; let result: prCiStatus.CommitStatus; try { result = await pRetry(async () => { const status = await prCiStatus.getCommitStatus(lastCommitSha); // Ensure that it wasn't cancelled/changed in the meanwhile if (waiting !== currentConfirmation) { throw new AbortError('The merge was cancelled or a new commit was pushed'); } if (status === prCiStatus.PENDING) { throw new Error('CI is not done yet'); } return status; }, { forever: true, minTimeout: 5000, maxTimeout: 10_000, }); } catch { return; } finally { disableForm(false); } if (result === prCiStatus.SUCCESS) { event.delegateTarget.classList.add('rgh-merging'); // Avoid triggering the event listener again event.delegateTarget.click(); } } let commitObserver: undefined | MutationObserver; function watchForNewCommits(): void { if (commitObserver) { return; } let previousCommit = prCiStatus.getLastCommitReference(); const filteredListener = (): void => { const newCommit = prCiStatus.getLastCommitReference(); if (newCommit === previousCommit) { return; } previousCommit = newCommit; // Cancel submission if a new commit was pushed disableForm(false); showCheckboxIfNecessary(); }; commitObserver = observeElement('.js-discussion', filteredListener, { childList: true, subtree: true, })!; } function onPrMergePanelHandler(): void { showCheckboxIfNecessary(); watchForNewCommits(); } function onBeforeunload(event: BeforeUnloadEvent): void { if (waiting) { event.returnValue = ''; } } function init(signal: AbortSignal): void { // Warn user if it's not yet submitted window.addEventListener('beforeunload', onBeforeunload, {signal}); onPrMergePanelLoad(onPrMergePanelHandler, signal); onPrMergePanelOpen(onPrMergePanelHandler, signal); // One of the merge buttons has been clicked delegate('.js-merge-commit-button:not(.rgh-merging)', 'click', handleMergeConfirmation, {signal}); // Cancel wait when the user presses the Cancel button delegate('.commit-form-actions button:not(.js-merge-commit-button)', 'click', () => { disableForm(false); }, {signal}); if (commitObserver) { onAbort(signal, commitObserver); } // Disable the feature under certain conditions. // These conditions cannot go in the `exclude` array because the mergeability box is loaded asynchronously observe([ // If it has a native Merge Queue behavior '.js-auto-merge-box', // If the PR requires administrator privileges https://github.com/refined-github/refined-github/issues/1771#issuecomment-1092415019 'input.js-admin-merge-override[type="checkbox"]', ], () => { getCheckbox()?.remove(); features.unload(import.meta.url); }, {signal}); } void features.add(import.meta.url, { asLongAs: [ isHasSelectorSupported, userCanLikelyMergePR, pageDetect.isOpenPR, // The repo has enabled Actions () => select.exists(actionsTab), ], include: [ pageDetect.isPRConversation, ], exclude: [ pageDetect.isDraftPR, ], awaitDomReady: true, // DOM-based inclusions init, }); /* Test URLs Checks: https://github.com/refined-github/sandbox/pull/12 No Checks: https://github.com/refined-github/sandbox/pull/10 */