import React from 'dom-chef'; import select from 'select-dom'; import alertIcon from 'octicon/alert.svg'; import delegate, {DelegateEvent} from 'delegate-it'; import features from '../libs/features'; import * as api from '../libs/api'; import observeEl from '../libs/simplified-element-observer'; import {getRepoURL, getDiscussionNumber} from '../libs/utils'; let observer: MutationObserver; function getBranches(): {base: string; head: string} { return { base: select('.base-ref')!.textContent!.trim(), head: select('.head-ref')!.textContent!.trim() }; } export async function mergeBranches(): Promise { return api.v3(`repos/${getRepoURL()}/pulls/${getDiscussionNumber()!}/update-branch`, { method: 'PUT', headers: { Accept: 'application/vnd.github.lydian-preview+json' }, ignoreHTTPStatus: true }); } async function handler(event: DelegateEvent): Promise { const button = event.target as HTMLButtonElement; button.disabled = true; button.textContent = 'Updating branch…'; button.classList.remove('tooltipped'); observer.disconnect(); const response = await mergeBranches(); if (response.ok) { button.remove(); } else if (response.message?.toLowerCase().startsWith('merge conflict')) { // Only shown on Draft PRs button.replaceWith( {alertIcon()} Resolve conflicts ); } else { button.textContent = response.message ?? 'Error'; button.prepend(alertIcon(), ' '); throw new api.RefinedGitHubAPIError('update-pr-from-base-branch: ' + JSON.stringify(response)); } } function createButton(base: string, head: string): HTMLElement { return ( ); } async function addButton(): Promise { if (select.exists('.rgh-update-pr-from-master, .branch-action-btn:not([action$="ready_for_review"]) > .btn')) { return; } const stillLoading = select('#partial-pull-merging poll-include-fragment'); if (stillLoading) { stillLoading.addEventListener('load', addButton); return; } const {base, head} = getBranches(); // Draft PRs already have this info on the page const [outOfDateContainer] = select.all('.completeness-indicator-problem + .status-heading') .filter(title => (title.textContent!).includes('out-of-date')); if (outOfDateContainer) { outOfDateContainer.append(createButton(base, head)); return; } const {status} = await api.v3(`repos/${getRepoURL()}/compare/${base}...${head}`); if (status !== 'diverged') { return; } for (const heading of select.all('.mergeability-details > :not(.js-details-container) .status-heading')) { heading.append(createButton(base, head)); } } function init(): void | false { // Button exists when the current user can merge the PR. // Button is disabled when: // - There are conflicts (there's already a native "Resolve conflicts" button) // - Draft PR (show the button anyway) const canMerge = select.exists('[data-details-container=".js-merge-pr"]:not(:disabled)'); const isDraftPR = select.exists('[action$="ready_for_review"]'); if (!canMerge && !isDraftPR) { return false; } observer = observeEl('.discussion-timeline-actions', addButton)!; delegate('.discussion-timeline-actions', '.rgh-update-pr-from-master', 'click', handler); } features.add({ id: __featureName__, description: 'Adds button to update a PR from the base branch to ensure it builds correctly before merging the PR itself.', screenshot: 'https://user-images.githubusercontent.com/1402241/57941992-f2170080-7902-11e9-8f8a-594aad983559.png', include: [ features.isPRConversation ], load: features.onAjaxedPages, init });