Skip to content
df4083e Jul 30, 2014
@nparry @tombell @docwhat
141 lines (125 sloc) 6.68 KB
# Description:
# Interact with Gerrit. (http://code.google.com/p/gerrit/)
#
# Dependencies:
#
# Configuration:
# HUBOT_GERRIT_SSH_URL
# HUBOT_GERRIT_EVENTSTREAM_ROOMS
#
# Commands:
# hubot gerrit search <query> - Search Gerrit for changes - the query should follow the normal Gerrit query rules
# hubot gerrit (ignore|report) events for (project|user|event) <thing> - Tell Hubot how to report Gerrit events
#
# Notes:
# Hubot has to be running as a user who has registered a SSH key with Gerrit
#
# Author:
# nparry
cp = require "child_process"
url = require "url"
# Required - The SSH URL for your Gerrit server.
sshUrl = process.env.HUBOT_GERRIT_SSH_URL || ""
# Optional - A comma separated list of rooms to receive spam about Gerrit events.
# If not set, messages will be sent to all room of which Hubot is a member.
# To disable event stream spam, use the value "disabled"
eventStreamRooms = process.env.HUBOT_GERRIT_EVENTSTREAM_ROOMS
# TODO: Make these template driven with env-var overrides possible.
# See the following for descriptions of the input JSON data:
# http://gerrit-documentation.googlecode.com/svn/Documentation/2.4/json.html
# http://gerrit-documentation.googlecode.com/svn/Documentation/2.4/cmd-stream-events.html
formatters =
queryResult: (json) -> "'#{json.change.subject}' for #{json.change.project}/#{json.change.branch} by #{extractName json.change} on #{formatDate json.change.lastUpdated}: #{json.change.url}"
events:
"patchset-created": (json) -> "#{extractName json} uploaded patchset #{json.patchSet.number} of '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}"
"change-abandoned": (json) -> "#{extractName json} abandoned '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}"
"change-restored": (json) -> "#{extractName json} restored '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}"
"change-merged": (json) -> "#{extractName json} merged patchset #{json.patchSet.number} of '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}"
"comment-added": (json) -> "#{extractName json} reviewed patchset #{json.patchSet.number} (#{extractReviews json}) of '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}"
"ref-updated": (json) -> "#{extractName json} updated reference #{json.refUpdate.project}/#{json.refUpdate.refName}"
formatDate = (seconds) -> new Date(seconds * 1000).toDateString()
extractName = (json) ->
account = json.uploader || json.abandoner || json.restorer || json.submitter || json.author || json.owner
account?.name || account?.email || "Gerrit"
extractReviews = (json) ->
("#{a.description}=#{a.value}" for a in json.approvals).join ","
module.exports = (robot) ->
gerrit = url.parse sshUrl
gerrit.port = 22 unless gerrit.port
if gerrit.protocol != "ssh:" || gerrit.hostname == ""
robot.logger.error "Gerrit commands inactive because HUBOT_GERRIT_SSH_URL=#{gerrit.href} is not a valid SSH URL"
else
eventStreamMe robot, gerrit unless eventStreamRooms == "disabled"
robot.respond /gerrit (?:search|query)(?: me)? (.+)/i, searchMe robot, gerrit
robot.respond /gerrit (ignore|report)(?: me)? events for (project|user|event) (.+)/i, ignoreOrReportEventsMe robot, gerrit
searchMe = (robot, gerrit) -> (msg) ->
cp.exec "ssh #{gerrit.hostname} -p #{gerrit.port} gerrit query --format=JSON -- #{msg.match[1]}", (err, stdout, stderr) ->
if err
msg.send "Sorry, something went wrong talking with Gerrit: #{stderr}"
else
results = (JSON.parse l for l in stdout.split "\n" when l isnt "")
status = results[results.length - 1]
if status.type == "error"
msg.send "Sorry, Gerrit didn't like your query: #{status.message}"
else if status.rowCount == 0
msg.send "Gerrit didn't find anything matching your query"
else
msg.send formatters.queryResult change: r for r in results when r.id
ignoreOrReportEventsMe = (robot, gerrit) -> (msg) ->
type = msg.match[2].toLowerCase()
thing = msg.match[3]
ignores = (t for t in ignoresOfType robot, type when t isnt thing)
ignores.push thing if msg.match[1] == "ignore"
robot.brain.data.gerrit ?= { }
robot.brain.data.gerrit.eventStream ?= { }
robot.brain.data.gerrit.eventStream.ignores ?= { }
robot.brain.data.gerrit.eventStream.ignores[type] = ignores
msg.send "Got it, the updated list of Gerrit #{type}s to ignore is #{ignores.join(', ') || 'empty'}"
eventStreamMe = (robot, gerrit) ->
robot.logger.info "Gerrit stream-events: Starting connection"
streamEvents = cp.spawn "ssh", [gerrit.hostname, "-p", gerrit.port, "gerrit", "stream-events"]
done = false
reconnect = null
robot.brain.on "close", ->
done = true
clearTimeout reconnect if reconnect
streamEvents.stdin.end()
streamEvents.on "exit", (code) ->
robot.logger.info "Gerrit stream-events: Connection lost (rc=#{code})"
reconnect = setTimeout (-> eventStreamMe robot, gerrit), 10 * 1000 unless done
isIgnored = (type, thing) -> (t for t in ignoresOfType robot, type when t is thing).length != 0
isWanted = (event) -> !(
isIgnored("project", (event.change || event.refUpdate).project) ||
isIgnored("user", extractName event) ||
isIgnored("event", event.type))
streamEvents.stderr.on "data", (data) ->
robot.logger.info "Gerrit stream-events: #{data}"
streamEvents.stdout.on "data", (data) ->
robot.logger.debug "Gerrit stream-events: #{data}"
json = try
JSON.parse data
catch error
robot.logger.error "Gerrit stream-events: Error parsing Gerrit JSON. Error=#{error}, Event=#{data}"
null
return unless json
formatter = formatters.events[json.type]
msg = try
formatter json if formatter
catch error
robot.logger.error "Gerrit stream-events: Error formatting event. Error=#{error}, Event=#{data}"
null
if formatter == null
robot.logger.info "Gerrit stream-events: Unrecognized event #{data}"
else if msg && isWanted json
# Bug in messageRoom? Doesn't work with multiple rooms
#robot.messageRoom room, "Gerrit: #{msg}" for room in robotRooms robot
robot.send room: room, "Gerrit: #{msg}" for room in robotRooms robot
ignoresOfType = (robot, type) -> robot.brain.data.gerrit?.eventStream?.ignores?[type] || []
robotRooms = (robot) ->
roomlists =
if eventStreamRooms
[ eventStreamRooms ]
else
v for k,v of process.env when /^HUBOT_.+_ROOMS/i.exec(k) isnt null
robot.logger.error "Gerrit stream-events: Unable to determine the list of rooms" if roomlists.length == 0
r for r in (roomlists[0] || "").split "," when r isnt ""
Something went wrong with that request. Please try again.