hubot-code-review
Version:
A Hubot script for GitHub code review on Slack [archived]
380 lines (345 loc) • 14.7 kB
text/coffeescript
# Description:
# Manage code review reminders
#
# Configuration:
# see README.md
#
# Commands:
# hubot help crs - display code review help
CodeReviews = require './CodeReviews'
CodeReview = require './CodeReview'
CodeReviewKarma = require './CodeReviewKarma'
msgRoomName = require './lib/msgRoomName'
module.exports = (robot) ->
code_review_karma = new CodeReviewKarma robot
code_reviews = new CodeReviews robot
enqueue_code_review = (msg) ->
# Ignore all bot messages
if msg.message.user?.slack?.is_bot
return
url = msg.match[1]
slug = code_reviews.matches_to_slug msg.match
msgRoomName msg, (room_name) ->
if slug and room_name
cr = new CodeReview msg.message.user, slug, url, room_name, msg.message.room
found = code_reviews.find_slug_index room_name, slug
if found is false
# 'Take' a code review for karma
code_review_karma.incr_score msg.message.user.name, 'take'
if (msg.match[5])? and msg.match[5].length
notification_string = msg.match[5].replace /^\s+|\s+$/g, ""
else
notification_string = null
# Add any extra info to the cr, seng extra notifications, and add it to the room_queue
code_reviews.add_cr_with_extra_info(cr, msg, notification_string)
else
if code_reviews.room_queues[room_name][found].status != 'new'
statusMsg = "#{code_reviews.room_queues[room_name][found].status}"
else
statusMsg = 'added'
if code_reviews.room_queues[room_name][found].reviewer
reviewerMsg = " and was #{statusMsg} by" +
" @#{code_reviews.room_queues[room_name][found].reviewer}"
else
reviewerMsg = ''
msg.send "*#{slug}* is already in the queue#{reviewerMsg}"
else
msg.send "Unable to add #{url} to queue. Please try again."
# Respond to message with matching slug names
#
# @param slugs matching slugs
# @param msg message to reply to
# @return none
send_be_more_specific = (slugs, msg) ->
# Bold the slugs
slugs = ("`#{slug}`" for slug in slugs)
lastSlug = 'or ' + slugs.pop()
slugs.push lastSlug
msg.send "You're gonna have to be more specific: " + slugs.join(', ') + '?'
# Return a single matching CR for slug match or alert the user to match status
#
# @param slugs matching slugs
# @param msg message to reply to
# @return none
single_matching_cr = (slug_to_search_for, room_name, msg, status = false, no_reply = false) ->
# search for matching slugs whether a fragment or full slug is provided
found_crs = code_reviews.search_room_by_slug room_name, slug_to_search_for, status
# no matches
if found_crs.length is 0
unless no_reply
status_prs = if status then "#{status} " else ''
msg.send "Sorry, I couldn't find any #{status_prs}PRs" +
" in this room matching `#{slug_to_search_for}`."
return
# multiple matches
else if found_crs.length > 1
foundSlugs = for cr in found_crs
cr.slug
unless no_reply
send_be_more_specific foundSlugs, msg
return
# There's a single matching slug in this room to redo
else
return found_crs[0]
dequeue_code_review = (cr, reviewer, msg) ->
if cr and cr.slug
code_review_karma.incr_score reviewer, 'give'
msg.send "Thanks, #{reviewer}! I removed *#{cr.slug}* from the code review queue."
###
@command hubot: help crs
@desc Display help docs for code review system
###
robot.respond /help crs(?: --(flush))?$/i, id: 'crs.help', (msg) ->
if ! code_reviews.help_text or (msg.match[1] and msg.match[1].toLowerCase() is 'flush')
code_reviews.set_help_text()
msg.send code_reviews.help_text
###
@command {GitHub pull request URL} [@user]
@desc Add PR to queue and (optionally) notify @user or #channel
###
robot.hear code_reviews.pr_url_regex, enqueue_code_review
###
@command [hubot: ]on it
@desc Claim the oldest _new_ PR in the queue
@command [hubot: ]userName is on it
@desc Tell hubot that userName has claimed the oldest _new_ PR in the queue
###
# Claim first PR in queue by directly addressing hubot
robot.respond /(?:([-_a-z0-9]+) is )?on it/i, (msg) ->
reviewer = msg.match[1] or msg.message.user.name
msgRoomName msg, (room_name) ->
if room_name
cr = code_reviews.claim_first room_name, reviewer
dequeue_code_review cr, reviewer, msg
# Claim first PR in queue wihout directly addressing hubot
# Note the this is a `hear` listener and previous is a `respond`
robot.hear /^(?:([-_a-z0-9]+) is )?on it$/, (msg) ->
# Ignore all bot messages
if msg.message.user?.slack?.is_bot
return
reviewer = msg.match[1] or msg.message.user.name
msgRoomName msg, (room_name) ->
if room_name
cr = code_reviews.claim_first room_name, reviewer
dequeue_code_review cr, reviewer, msg
###
@command on *
@desc Claim all _new_ PRs
###
robot.hear /^on \*$/i, (msg) ->
# Ignore all bot messages
if msg.message.user?.slack?.is_bot
return
msg.emote ":tornado2:"
reviewer = msg.message.user.name
msgRoomName msg, (room_name) ->
if room_name
until false is cr = code_reviews.claim_first room_name, reviewer
dequeue_code_review cr, reviewer, msg
###
@command [userName is ]on cool-repo/123
@desc Claim `cool-repo/123` if no one else has claimed it
@command [userName is ]on cool
@desc Claim a _new_ PR whose slug matches `cool`
###
robot.hear /^(?:([-_a-z0-9]+) is )?(?:on) ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+)$/i, (msg) ->
# Ignore all bot messages
if msg.message.user?.slack?.is_bot
return
reviewer = msg.match[1] or msg.message.user.name
slug = msg.match[2]
return if slug.toLowerCase() is 'it'
msgRoomName msg, (room_name) ->
if room_name
unclaimed_cr = single_matching_cr(slug, room_name, msg, status = "new")
if (unclaimed_cr)?
code_reviews.claim_by_slug room_name, unclaimed_cr.slug, reviewer
dequeue_code_review unclaimed_cr, reviewer, msg
# none of the matches have "new" status
else
cr = single_matching_cr(slug, room_name, msg, status = false, no_output = true)
# When someone attempts to claim a PR
# that was already reviewed, merged, or closed outside of the queue
if (cr)?
response = "It looks like *#{cr.slug}* (@#{cr.user.name}) has already been #{cr.status}"
msg.send response
###
@command hubot (nm|ignore) cool-repo/123
@desc Delete `cool-repo/123` from queue regardless of status
@command hubot (nm|ignore) cool
@desc Delete most recently added PR whose slug matches `cool` regardless of status
###
robot.respond /(?:nm|ignore) ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+)$/i, (msg) ->
slug = msg.match[1]
return if slug.toLowerCase() is 'it'
msgRoomName msg, (room_name) ->
if room_name
found_ignore_cr = single_matching_cr(slug, room_name, msg)
if (found_ignore_cr)?
code_reviews.remove_by_slug room_name, found_ignore_cr.slug
#decrement scores
code_review_karma.decr_score found_ignore_cr.user.name, 'take'
if found_ignore_cr.reviewer
code_review_karma.decr_score found_ignore_cr.reviewer, 'give'
msg.send "Sorry for eavesdropping. I removed *#{found_ignore_cr.slug}* from the queue."
return
###
@command hubot: (nm|ignore)
@desc Delete most recently added PR from the queue regardless of status
###
robot.respond /(?:\s*)(?:nm|ignore)(?:\s*)$/i, (msg) ->
msgRoomName msg, (room_name) ->
if room_name
cr = code_reviews.remove_last_new room_name
if cr and cr.slug
code_review_karma.decr_score cr.user.name, 'take'
if cr.reviewer
code_review_karma.decr_score cr.reviewer, 'give'
msg.send "Sorry for eavesdropping. I removed *#{cr.slug}* from the queue."
else
msg.send "There might not be a new PR to remove. Try specifying a slug."
###
@command hubot: redo cool-repo/123
@desc Allow another review _without_ decrementing previous reviewer's score
###
robot.respond /(?:redo)(?: ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+))/i, (msg) ->
msgRoomName msg, (room_name) ->
if room_name
found_redo_cr = single_matching_cr(msg.match[1], room_name, msg)
if (found_redo_cr)?
index = code_reviews.find_slug_index room_name, found_redo_cr.slug
code_reviews.reset_cr code_reviews.room_queues[room_name][index]
msg.send "You got it, #{found_redo_cr.slug} is ready for a new review."
###
@command hubot: (unclaim|reset) cool-repo/123
@desc Reset CR status to new/unclaimed _and_ decrement reviewer's score
###
robot.respond /(unclaim|reset)(?: ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+))?/i, (msg) ->
msgRoomName msg, (room_name) ->
if room_name
found_reset_cr = single_matching_cr(msg.match[2], room_name, msg)
if (found_reset_cr)?
# decrement reviewers CR score
if found_reset_cr.reviewer
code_review_karma.decr_score found_reset_cr.reviewer, 'give'
index = code_reviews.find_slug_index room_name, found_reset_cr.slug
code_reviews.reset_cr code_reviews.room_queues[room_name][index]
msg.match[1] += 'ed' if msg.match[1].toLowerCase() is 'unclaim'
msg.send "You got it, I've #{msg.match[1]} *#{found_reset_cr.slug}* in the queue."
###
@command hubot: list crs
@desc List all _unclaimed_ CRs in the queue
@command hubot: list [status] crs
@desc List CRs with matching optional status
###
robot.respond /list(?: (all|new|claimed|approved|closed|merged))? CRs/i, (msg) ->
status = msg.match[1] || 'new'
msgRoomName msg, (room_name) ->
if room_name
code_reviews.send_list room_name, false, status
# Flush all CRs in all rooms
robot.respond /flush the cr queue, really really/i, (msg) ->
code_reviews.flush_queues()
msg.send "This house is clear"
# Display JSON of all CR queues
robot.respond /debug the cr queue ?(?:for #?([a-z0-9\-_]+))?$/i, (msg) ->
if !msg.match[1]
msg.send code_reviews.queues_debug_stats()
else
msg.send code_reviews.queues_debug_room(msg.match[1])
# Mark a CR as approved or closed when webhook received from GitHub
robot.router.post '/hubot/hubot-code-review', (req, res) ->
# check header
unless req.headers['x-github-event']
res.statusCode = 400
res.send 'x-github-event is required'
return
# Check if PR was approved (via emoji in issue_comment body)
if req.headers['x-github-event'] is 'issue_comment' and
req.body.comment.user.type != 'Bot' # Commenter is not a bot
if ((process.env.HUBOT_CODE_REVIEW_EMOJI_APPROVE?) and
process.env.HUBOT_CODE_REVIEW_EMOJI_APPROVE)
if (code_reviews.emoji_regex.test(req.body.comment.body) or
code_reviews.emoji_unicode_test(req.body.comment.body))
code_reviews.approve_cr_by_url(
req.body.issue.html_url,
req.body.comment.user.login,
req.body.comment.body
)
response = "issue_comment approved #{req.body.issue.html_url}"
else
code_reviews.comment_cr_by_url(
req.body.issue.html_url,
req.body.comment.user.login,
req.body.comment.body
)
response = "issue_comment did not yet approve #{req.body.issue.html_url}"
else
code_reviews.comment_cr_by_url(
req.body.issue.html_url,
req.body.comment.user.login,
req.body.comment.body
)
response = "issue_comment did not yet approve #{req.body.issue.html_url}"
# Check if PR was merged or closed
else if req.headers['x-github-event'] is 'pull_request'
if req.body.action is 'closed'
# update CRs
status = if req.body.pull_request.merged then 'merged' else 'closed'
updated = code_reviews.handle_close req.body.pull_request.html_url, status
# build response message
if updated.length
response = "set status of #{updated[0].slug} to "
rooms = for cr in updated
"#{cr.status} in #{cr.room}"
response += rooms.join(', ')
else
response = "#{req.body.pull_request.html_url} not found in any queue"
else
response = "#{req.body.pull_request.html_url} is still open"
# Check if PR was approved via GitHub's Pull Request Review
else if req.headers['x-github-event'] is 'pull_request_review' and
req.body.review.user.type != 'Bot' # not a bot
if req.body.action? and req.body.action is 'dismissed'
response = "pull_request_review dismissed #{req.body.pull_request.html_url}"
code_reviews.dismiss_cr_by_url(
req.body.pull_request.html_url,
req.body.review.user.login
)
else
if req.body.review.state is 'approved'
response = "pull_request_review approved #{req.body.pull_request.html_url}"
code_reviews.approve_cr_by_url(
req.body.pull_request.html_url,
req.body.review.user.login,
req.body.review.body
)
else if req.body.review.state is 'changes_requested'
response = "pull_request_review changes requested #{req.body.pull_request.html_url}"
# Send the changes requested comment to the submitter
if req.body.review.body?
code_reviews.comment_cr_by_url(
req.body.pull_request.html_url,
req.body.review.user.login,
req.body.review.body
)
# Close up the PR and notify the submitter to resubmit when changes are made
code_reviews.handle_close(
req.body.pull_request.html_url,
'changes_requested'
)
else
response = "pull_request_review not yet approved #{req.body.pull_request.html_url}"
if req.body.review.body?
code_reviews.comment_cr_by_url(
req.body.pull_request.html_url,
req.body.review.user.login,
req.body.review.body
)
else
res.statusCode = 400
response = "invalid x-github-event #{req.headers['x-github-event']}"
# useful for testing
res.send response
# return for use in unit tests
return code_reviews