Skip to content
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ GitHub Enterprise is also supported. More info in the options.

### Added features

- [Lets you wait for a build before automatically merging a PR](https://user-images.githubusercontent.com/1402241/35192861-3f4a1bf6-fecc-11e7-8b9f-35ee019c6cdf.gif)
- Toggle all [outdated PR comments](https://user-images.githubusercontent.com/25818354/33240033-3e271588-d2af-11e7-93af-13b6e325f65d.gif) or [PR files](https://user-images.githubusercontent.com/1402241/35192652-6f79dc42-fec9-11e7-89ad-2b4a9c5f4f52.gif) with <kbd>Alt</kbd>+click
- Copy canonical link to file when [the <kbd>y</kbd> hotkey](https://help.github.com/articles/getting-permanent-links-to-files/) is used
- Supports indenting with the tab key in textareas like the comment box (<kbd>Shift</kbd> <kbd>Tab</kbd> for original behavior)
Expand Down
14 changes: 14 additions & 0 deletions source/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@
display: none !important;
}

/* Visibly disable merge fields during submission */
[name='commit_message'][disabled],
[name='commit_title'][disabled] {
color: #808080;
}

/* Remove top buttons on comment box */
.timeline-comment-wrapper .tabnav-extra,
.inline-comment-form-container .tabnav-extra {
Expand Down Expand Up @@ -559,6 +565,14 @@ div.inline-comment-form .form-actions,
float: none;
}

/* ...in the merge confirmation form */
.commit-form-actions .select-menu {
display: block !important;
}
.commit-form-actions button:not(.js-merge-commit-button) {
float: right;
}

/* Decrease the size of the search box to fit 'Yours' menu item */
.subnav-search-input[aria-label='Search all issues'] {
width: 420px;
Expand Down
2 changes: 2 additions & 0 deletions source/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import extendDiffExpander from './features/extend-diff-expander';
import sortIssuesByUpdateTime from './features/sort-issues-by-update-time';
import makeDiscussionSidebarSticky from './features/make-discussion-sidebar-sticky';
import shortenLinks from './features/shorten-links';
import waitForBuild from './features/wait-for-build';
import addDownloadFolderButton from './features/add-download-folder-button';
import hideUselessNewsfeedEvents from './features/hide-useless-newsfeed-events';
import addScopedSearchOnUserProfile from './features/add-scoped-search-on-user-profile';
Expand Down Expand Up @@ -171,6 +172,7 @@ function ajaxedPagesHandler() {
enableFeature(addDeleteForkLink);
enableFeature(fixSquashAndMergeTitle);
enableFeature(openCIDetailsInNewTab);
enableFeature(waitForBuild);
enableFeature(toggleAllThingsWithAlt);
}

Expand Down
101 changes: 101 additions & 0 deletions source/features/wait-for-build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {h} from 'dom-chef';
import select from 'select-dom';
import onetime from 'onetime';
import delegate from 'delegate';
import {observeEl} from '../libs/utils';
import * as prCiStatus from '../libs/pr-ci-status';

let waiting;

// Reuse the same checkbox to preserve its status
const generateCheckbox = onetime(() => (
<label class="d-inline-block">
<input type="checkbox" name="rgh-pr-check-waiter" checked/>
{' Wait for successful checks'}
</label>
));

function canMerge() {
return select.exists('.merge-message [type="submit"]:not(:disabled)');
}

function getCheckbox() {
return select('[name="rgh-pr-check-waiter"]');
}

// Only show the checkbox if there's a pending commit
function showCheckboxIfNecessary() {
const checkbox = getCheckbox();
const isNecessary = prCiStatus.get() === prCiStatus.PENDING;
if (!checkbox && isNecessary) {
const container = select('.commit-form-actions .select-menu');
if (container) {
container.append(generateCheckbox());
}
} else if (checkbox && !isNecessary) {
checkbox.parentNode.remove();
}
}

function disableForm(disabled = true) {
for (const field of select.all(`
[name="commit_message"],
[name="commit_title"],
[name="rgh-pr-check-waiter"],
.js-merge-commit-button
`)) {
field.disabled = disabled;
}

// Enabled form = no waiting in progress
if (!disabled) {
waiting = undefined;
}
}

async function handleMergeConfirmation(event) {
const checkbox = getCheckbox();
if (checkbox && checkbox.checked) {
event.preventDefault();

disableForm();
const currentConfirmation = Symbol('');
waiting = currentConfirmation;
const status = await prCiStatus.wait();

// Ensure that it wasn't cancelled/changed in the meanwhile
if (waiting === currentConfirmation) {
disableForm(false);

if (status === prCiStatus.SUCCESS) {
event.target.click();
}
}
}
}

export default function () {
if (canMerge() && !select.exists('.rgh-wait-for-build')) {
document.body.classList.add('rgh-wait-for-build');

const container = select('.discussion-timeline-actions');

// The merge form is regenerated by GitHub on every update
observeEl(container, showCheckboxIfNecessary);

// Watch for new commits and their statuses
prCiStatus.addEventListener(showCheckboxIfNecessary);

// One of the merge buttons has been clicked
delegate(container, '.js-merge-commit-button', 'click', handleMergeConfirmation);

// Cancel wait when the user presses the Cancel button
delegate(container, '.commit-form-actions button:not(.js-merge-commit-button)', 'click', () => {
disableForm(false);
});

// Warn user if it's not yet submitted.
// Sadly the message isn't shown
window.onbeforeunload = () => waiting && 'The PR hasn’t merged yet.';
}
}
71 changes: 71 additions & 0 deletions source/libs/pr-ci-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import select from 'select-dom';
import {observeEl} from './utils';

function getLastCommit() {
return select.all('.timeline-commits .commit-id').pop().textContent;
}

export const SUCCESS = Symbol('Success');
export const FAILURE = Symbol('Failure');
export const PENDING = Symbol('Pending');
export const COMMIT_CHANGED = Symbol('Commit changed');

export function get() {
const commits = select.all('.commit-build-statuses > :first-child');
const lastCommit = commits[commits.length - 1];
if (lastCommit) {
if (lastCommit.matches('.text-green')) {
return SUCCESS;
}
if (lastCommit.matches('.text-red')) {
return FAILURE;
}
return PENDING;
}
return false;
}

export function wait() {
return new Promise(resolve => {
addEventListener(function handler(newStatus) {
removeEventListener(handler);
resolve(newStatus);
});
});
}

const observers = new WeakMap();

export function addEventListener(listener) {
if (observers.has(listener)) {
return;
}

let previousCommit = getLastCommit();
let previousStatus = get();
const filteredListener = () => {
// Cancel submission if a new commit was pushed
const newCommit = getLastCommit();
if (newCommit !== previousCommit) {
previousCommit = newCommit;
listener(COMMIT_CHANGED);
}

// Ignore update if the status hasn't changed
const newStatus = get();
if (newStatus !== previousStatus) {
previousStatus = newStatus;
listener(newStatus);
}
};

const observer = observeEl('.js-discussion', filteredListener, {
childList: true,
subtree: true
});
observers.set(listener, observer);
}

export function removeEventListener(listener) {
observers.get(listener).disconnect();
}