Skip to content

Instantly share code, notes, and snippets.

@GrayHatter
Created January 6, 2017 10:34
Show Gist options
  • Save GrayHatter/0aa943cab7742c03ef741c1328d298c3 to your computer and use it in GitHub Desktop.
Save GrayHatter/0aa943cab7742c03ef741c1328d298c3 to your computer and use it in GitHub Desktop.
// This is the built-in review completion condition that
// Reviewable uses by default.
// It checks that all files have been reviewed by at least one
// user and that all discussions have been resolved. All the
// information about the current review is supplied in a
// predefined `review` variable that will look like the JSON
// structure on the right. You can edit it here for testing
// how the code will behave in different scenarios.
// You can load other examples from the dropdown menu above.
let reasons = []; // pieces of the status description
let shortReasons = []; // pieces of the short status desc.
const summary = review.summary; // shortcut to summary
const completed =
!summary.numUnresolvedDiscussions &&
!summary.numUnreviewedFiles;
if (summary.numUnreviewedFiles) {
reasons.push(
(summary.numFiles - summary.numUnreviewedFiles) +
' of ' + summary.numFiles + ' files reviewed');
shortReasons.push(
summary.numUnreviewedFiles + ' file' +
(summary.numUnreviewedFiles > 1 ? 's' : '')
);
} else {
reasons.push('all files reviewed');
}
if (summary.numUnresolvedDiscussions) {
reasons.push(
summary.numUnresolvedDiscussions +
' unresolved discussion' +
(summary.numUnresolvedDiscussions > 1 ? 's' : ''));
shortReasons.push(
summary.numUnresolvedDiscussions + ' discussion' +
(summary.numUnresolvedDiscussions > 1 ? 's' : '')
);
} else {
reasons.push('all discussions resolved');
}
let discussionBlockers = _(review.discussions)
.where({resolved: false})
.pluck('participants')
.flatten()
.where({resolved: false})
.map(user => _.pick(user, 'username'))
.value();
let fileBlockers = _(review.files)
.filter(file => _.isEmpty(_.last(file.revisions).reviewers))
.map(file => _(file.revisions).findLast(
rev => !_.isEmpty(rev.reviewers)))
.compact()
.pluck('reviewers')
.flatten()
.value();
if (!completed && _.some(fileBlockers, user => !user)) {
fileBlockers =
fileBlockers.concat(review.pullRequest.assignees);
}
let shortDescription;
if (completed) {
shortDescription =
summary.numFiles + ' file' +
(summary.numFiles > 1 ? 's' : '') + ' reviewed';
} else {
shortDescription = shortReasons.join(', ') + ' left';
}
return {
completed,
description: reasons.join(', '),
shortDescription,
pendingReviewers:
_.uniq(fileBlockers.concat(discussionBlockers), 'username')
};
// This code will check that all discussions have been resolved,
// and all files reviewed at the latest revision by all the
// assignees and by at least the required number of people, while
// ignoring the pull request's author. It makes use of Lodash
// (_), which is supplied by the execution environment.
const numReviewersRequired = 1;
let completed = true;
let reasons = []; // pieces of the status description
let shortReasons = []; // pieces of the short status desc.
let fileBlockers = []; // users who still need to review files
const assignees = _(review.pullRequest.assignees)
.pluck('username')
.without(review.pullRequest.author.username)
.value();
let numUnreviewedFiles = 0;
_.each(review.files, function(file) {
const reviewers = _(_.last(file.revisions).reviewers)
.pluck('username')
.without(review.pullRequest.author.username)
.value();
const missingAssignees = _.difference(assignees, reviewers);
if (reviewers.length >= numReviewersRequired &&
_.isEmpty(missingAssignees)) return;
numUnreviewedFiles++;
const lastReviewedRev =
_(file.revisions).findLast(rev => !_.isEmpty(rev.reviewers));
fileBlockers = fileBlockers.concat(
_.map(missingAssignees, username => ({username})),
lastReviewedRev ? lastReviewedRev.reviewers : []
);
});
if (numUnreviewedFiles) {
completed = false;
reasons.push(
(review.summary.numFiles - numUnreviewedFiles) + ' of ' +
review.summary.numFiles + ' files reviewed');
shortReasons.push(
numUnreviewedFiles + ' file' +
(numUnreviewedFiles > 1 ? 's' : '')
);
} else {
reasons.push('all files reviewed');
}
if (review.summary.numUnresolvedDiscussions) {
completed = false;
reasons.push(
review.summary.numUnresolvedDiscussions +
' unresolved discussion' +
(review.summary.numUnresolvedDiscussions > 1 ? 's' : ''));
shortReasons.push(
review.summary.numUnresolvedDiscussions + ' discussion' +
(review.summary.numUnresolvedDiscussions > 1 ? 's' : '')
);
} else {
reasons.push('all discussions resolved');
}
let discussionBlockers = _(review.discussions)
.where({resolved: false})
.pluck('participants')
.flatten()
.where({resolved: false})
.map(user => _.pick(user, 'username'))
.value();
let shortDescription;
if (completed) {
shortDescription =
review.summary.numFiles + ' file' +
(review.summary.numFiles > 1 ? 's' : '') + ' reviewed';
} else {
shortDescription = shortReasons.join(', ') + ' left';
}
return {
completed,
description: reasons.join(', '),
shortDescription,
pendingReviewers:
_.uniq(fileBlockers.concat(discussionBlockers), 'username')
};
// This code will check that the pull request has been approved
// via LGTM (Looks Good To Me) emojis by a minimum number of
// reviewers and by all assignees.
//
// Approval is granted via the :lgtm: and :lgtm_strong: emojis,
// and can be withdrawn with :lgtm_cancel:. An :lgtm: is only
// good for the last non-provisional revision at the time the
// comment is sent, so any new commits will require another
// approval. An :lgtm_strong: is good for all revisions unless
// canceled.
// The number of LGTMs required to merge.
let numApprovalsRequired = 1;
// Approval by username: true if current LGTM, false if stale,
// missing if not given or canceled.
let approvals = {};
// Timestamp of the currently latest revision.
const lastRevisionTimestamp =
_.last(review.revisions).snapshotTimestamp;
_.each(review.sentiments, function(sentiment) {
const emojis = _.indexBy(sentiment.emojis);
if (emojis.lgtm_cancel) {
delete approvals[sentiment.username];
} else if (emojis.lgtm_strong) {
approvals[sentiment.username] = true;
} else if (emojis.lgtm && !approvals[sentiment.username]) {
approvals[sentiment.username] =
sentiment.timestamp >= lastRevisionTimestamp;
}
});
const numApprovals = _.countBy(approvals);
let numGranted = numApprovals.true || 0;
let pendingReviewers = [];
const assignees =
_.pluck(review.pullRequest.assignees, 'username');
if (assignees.length) {
numApprovalsRequired =
_.max([assignees.length, numApprovalsRequired]);
numGranted =
(_(approvals).pick(assignees).countBy().value().true || 0) +
_.min([numGranted, numApprovalsRequired - assignees.length]);
pendingReviewers =
_(assignees)
.reject(username => approvals[username])
.map(username => ({username}))
.value();
}
let description =
numGranted + ' of ' + numApprovalsRequired + ' LGTMs obtained';
let shortDescription =
numGranted + '/' + numApprovalsRequired + ' LGTMs';
if (numApprovals.false) {
description += ', and ' + numApprovals.false + ' stale';
shortDescription += ', ' + numApprovals.false + ' stale';
}
return {
completed: numGranted >= numApprovalsRequired,
description, shortDescription, pendingReviewers,
debug: approvals
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment