UNPKG

hubot-gerrit

Version:

Hubot script for Gerrit including channel subscriptions and Slack attachments

292 lines (262 loc) 11.2 kB
# Description: # Interact with Gerrit. (http://code.google.com/p/gerrit/) # # Dependencies: # # Configuration: # HUBOT_GERRIT_SSH_URL # # Commands: # hubot search gerrit _<query>_ - Search Gerrit for changes (limited to 3 results) # hubot show gerrit updates for _event_ _(patchset-created|change-abandoned|change-restored|change-merged)_ - Subscribe active channel to Gerrit updates # hubot show gerrit updates for _(project|user)_ _<update>_ - Subscribe active channel to Gerrit updates # hubot remove gerrit updates for _(project|user|event)_ _<update>_ - Remove Gerrit update from active channel # hubot view gerrit subscriptions - View gerrit subscriptions for active channel # # Notes: # Hubot has to be running as a user who has registered a SSH key with Gerrit # # Authors: # nparry, justmiles cp = require "child_process" url = require "url" # Required - The SSH URL for your Gerrit server. sshUrl = process.env.HUBOT_GERRIT_SSH_URL || "" attachments = queryResult: (json) -> { "fallback": "'#{json.change.subject}' for #{json.change.project}/#{json.change.branch} by #{extractName json.change} on #{formatDate json.change.lastUpdated}", "title": "#{json.change.project} by #{extractName json.change}", "title_link": json.change.url, "text": json.change.subject, "fields": [ { "title": "Branch", "value": json.change.branch, "short": true }, { "title": "Date", "value": "#{formatDate json.change.lastUpdated}", "short": true } ] } events: "patchset-created": (json) -> { "fallback": "#{extractName json} uploaded patchset #{json.patchSet.number} of '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}", "title": "Commit Created in #{json.change.project}", "title_link": json.change.url, "color": "a6a6a6", "text": json.change.subject, "fields": [ { "title": "Author", "value": "#{extractName json}", "short": true }, { "title": "Branch", "value": json.change.branch, "short": true } ] } "change-abandoned": (json) -> { "fallback": "#{extractName json} abandoned '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}", "title": "Commit Abandoned in #{json.change.project}", "title_link": json.change.url, "color": "f30020", "text": json.change.subject, "fields": [ { "title": "Abandoned by", "value": "#{extractName json}", "short": true }, { "title": "Branch", "value": json.change.branch, "short": true } ] } "change-restored": (json) -> { "fallback": "#{extractName json} restored '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}", "title": "Commit Restored in #{json.change.project}", "title_link": json.change.url, "color": "#F35A00", "text": json.change.subject, "fields": [ { "title": "Restored by", "value": "#{extractName json}", "short": true }, { "title": "Branch", "value": json.change.branch, "short": true } ] } "change-merged": (json) -> { "fallback": "#{extractName json} merged #{json.patchSet.number} of '#{json.change.subject}' for #{json.change.project}/#{json.change.branch}: #{json.change.url}", "title": "Commit Merged in #{json.change.project}", "title_link": json.change.url, "color": "#36a64f", "text": json.change.subject, "fields": [ { "title": "Merged by", "value": "#{extractName json}", "short": true }, { "title": "Branch", "value": json.change.branch, "short": true } ] } 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) -> timestamp = new Date(seconds * 1000); format = {hour: '2-digit', minute:'2-digit'} return "#{timestamp.toLocaleDateString()} #{timestamp.toLocaleTimeString('en-US', format)}"; asSet = (array) -> a = array.concat() i = 0 while i < a.length j = i + 1 while j < a.length if a[i] == a[j] a.splice j--, 1 ++j ++i return a 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 robot.respond /(?:search|query)(?: me)? gerrit (.+)/i, searchMe robot, gerrit robot.respond /(show)(?: me)? gerrit updates for (project|user|event) (.+)/i, subscribeToEvents robot robot.respond /(remove)(?: me)? gerrit updates for (project|user|event) (.+)/i, deleteSubscription robot robot.respond /view gerrit subscriptions/i, showSubscriptions robot searchMe = (robot, gerrit) -> (msg) -> cp.exec "ssh #{gerrit.hostname} -p #{gerrit.port} gerrit query --format=JSON -- #{msg.match[1]} limit:3", (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 if robot.adapterName == 'slack' for r in results when r.id robot.emit 'slack-attachment', message: room: msg.message.room content: attachments.queryResult change: r else msg.send formatters.queryResult change: r for r in results when r.id showSubscriptions = (robot) -> (msg) -> hasSubscriptions = false show = (obj, type) -> for k,v of obj if typeof v == 'object' if v.indexOf(msg.message.room) != -1 hasSubscriptions = true msg.send "#{msg.message.room} is subscribed to #{type} `#{k}`" show robot.brain.data.gerrit?.eventStream?.subscription?.user, 'user' show robot.brain.data.gerrit?.eventStream?.subscription?.event, 'event' show robot.brain.data.gerrit?.eventStream?.subscription?.project, 'project' if !hasSubscriptions msg.send "#{msg.message.room} has no subscriptions" deleteSubscription = (robot) -> (msg) -> type = msg.match[2].toLowerCase() event = msg.match[3] subscribers = robot.brain.data.gerrit?.eventStream?.subscription?[type]?[event] || [] index = subscribers.indexOf(msg.message.room) if index != -1 subscribers.splice(index, 1); robot.brain.data.gerrit?.eventStream?.subscription?[type]?[event] = subscribers msg.send "#{msg.message.room} will no longer receive #{event} updates." else msg.send "#{msg.message.room} was never subscribed to #{event} updates" subscribeToEvents = (robot) -> (msg) -> type = msg.match[2].toLowerCase() event = msg.match[3] subscribers = robot.brain.data.gerrit?.eventStream?.subscription?[type]?[event] || [] subscribers.push msg.message.room robot.brain.data.gerrit ?= { } robot.brain.data.gerrit.eventStream ?= { } robot.brain.data.gerrit.eventStream.subscription ?= { } robot.brain.data.gerrit.eventStream.subscription[type] ?= { } robot.brain.data.gerrit.eventStream.subscription[type][event] = asSet subscribers msg.send "This channel has subscribed to Gerrit #{type} updates matching `#{event}`" 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 getSubscribers = (robot, event) -> projectSubs = robot.brain.data.gerrit?.eventStream?.subscription?.project?[event.change.project] || [] eventSubs = robot.brain.data.gerrit?.eventStream?.subscription?.event?[event.type] || [] userSubs = robot.brain.data.gerrit?.eventStream?.subscription?.user?["#{extractName event}"] || [] return asSet projectSubs.concat(eventSubs).concat(userSubs) 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 if robot.adapterName == 'slack' formatter = attachments.events[json.type] else 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 for room in getSubscribers(robot, json) if robot.adapterName == 'slack' robot.emit 'slack-attachment', message: room: room content: msg else robot.send room: room, "Gerrit: #{msg}"