From 983c95e225b5f72bd847503a80731edc035e776c Mon Sep 17 00:00:00 2001 From: Scott Chacon Date: Sat, 3 May 2008 17:45:32 -0700 Subject: [PATCH] added rake tasks for lighthouse ticketing system automation the following tasks were added : * rake patch:setup # setup your token, TOKEN=XXXX.. * rake patch:list # list all open patch tickets * rake patch:show:ticket # show patch ticket, include ID=XX * rake patch:show:attach # outputs actual patch file, include ID=XX (and o... * rake patch:show:comments # lists all comments on a ticket and adds up the +1s * rake patch:comment # review a patch - comment and optionally +1 it (ID=XX) I would add a task to upload new attachments, but as far as I can tell, Lighthouse API has no way to do that. I'll try to figure that out next, probably. --- Rakefile | 1 + tasks/lighthouse.rake | 466 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 467 insertions(+), 0 deletions(-) create mode 100644 tasks/lighthouse.rake diff --git a/Rakefile b/Rakefile index 11d205a..1e6b8e8 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ require 'rake' +load 'tasks/lighthouse.rake' env = %(PKG_BUILD="#{ENV['PKG_BUILD']}") if ENV['PKG_BUILD'] diff --git a/tasks/lighthouse.rake b/tasks/lighthouse.rake new file mode 100644 index 0000000..c7bccf5 --- /dev/null +++ b/tasks/lighthouse.rake @@ -0,0 +1,466 @@ +require 'yaml' +require 'active_resource' +require 'active_support' +require 'hpricot' +require 'open-uri' +require 'pp' + +namespace :patch do + + LH_URL = "http://rails.lighthouseapp.com" + RAILS_PROJECT_ID = 8994 + CONFIG_FILE = File.expand_path(File.dirname(__FILE__) + "/../.rails_lighthouse.yml") + + def setup_env + Lighthouse.account = 'rails' + #Lighthouse.account = 'chacon' + if File.exists?(CONFIG_FILE) + config = YAML::load(File.open(CONFIG_FILE)) + Lighthouse.token = config['token'] + end + end + + def get_project + Lighthouse::Project.find(RAILS_PROJECT_ID) + end + + def get_patch_tickets + ts = Lighthouse::Ticket.find(:all, :params => { :project_id => RAILS_PROJECT_ID, + :q => "state:open tagged:patch" }) + end + + def get_url(tic) + LH_URL + "/projects/#{RAILS_PROJECT_ID}/tickets/#{tic.number}" + end + + def get_xml_url(ticket_id) + LH_URL + "/projects/#{RAILS_PROJECT_ID}/tickets/#{ticket_id}.xml" + end + + def word_wrap(text, line_width = 80, line_height = 15) + block = text.split("\n").collect do |line| + ' ' + ((line.length > line_width) ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, " \\1\n").strip : line) + end * "\n" + block_lines = block.split("\n") + if block_lines.size > line_height + block_lines[0, line_height].join("\n") + "\n ..." + else + block_lines.join("\n") + end + end + + def get_ticket(ticket_id) + if ticket_id + tickets = get_patch_tickets # cant figure out how to get a specific ticket + if t = tickets.select { |t| t.number.to_s == ticket_id } + if tic = t.first + return tic + end + end + end + return false + end + + def get_editor_message(message_file = nil) + message_file = Tempfile.new('rails_message').path if !message_file + + editor = ENV["EDITOR"] || 'vim' + system("#{editor} #{message_file}"); + message = File.readlines(message_file) + message = message.select { |line| line[0, 1] != '#' } # removing comments + if message.empty? + return false + else + return message + end + end + + desc 'setup your token, TOKEN=XXXX..' + task :setup do + setup_env + if token = ENV['TOKEN'] + f = File.open(CONFIG_FILE, 'w') + f.write({'token' => token}.to_yaml) + f.close + end + end + + # todo: accept other tags and filters + desc 'list all open patch tickets' + task :list do + setup_env + + tickets = [] + puts ['Date', 'Num', 'Attch', 'Title'].join("\t") + get_patch_tickets.each do |tic| + tickets << [tic.created_at.strftime("%m/%d"), tic.number, tic.attachments_count, tic.title[0, 70]] + end + tickets.sort! { |a, b| a[1] <=> b[1] } + puts tickets.map { |t| t.join("\t") }.join("\n") + end + + + namespace :show do + + desc 'show patch ticket, include ID=XX' + task :ticket do + setup_env + + if tic = get_ticket(ENV['ID']) + tic_url = get_url(tic) + puts + puts 'Title : ' + tic.title + puts 'Number : ' + tic.number.to_s + puts + puts 'URL : ' + tic_url + puts 'Created : ' + tic.created_at.to_s + puts 'State : ' + tic.state + puts 'Tags : ' + tic.tag + puts + puts word_wrap(tic.body) + puts + puts '-- Attachments --' + puts + + # load the url, because we have to scrape the attachments + doc = Hpricot(open(tic_url)) + counter = 0 + (doc/"ul.attachments"/:li/:ins/:h4/:a).each do |t| + counter += 1 + puts ' ' + counter.to_s + ' : ' + t['href'].split('/').last + end + end + end + + desc 'outputs actual patch file, include ID=XX (and optionally NUMBER=XX if more than 1)' + task :attach do + setup_env + + if tic = get_ticket(ENV['ID']) + doc = Hpricot(open(get_url(tic))) + urls = [] + (doc/"ul.attachments"/:li/:ins/:h4/:a).each do |t| + urls << LH_URL + t['href'] + end + idx = ((urls.size > 1) && (ENV['NUMBER'])) ? ENV['NUMBER'].to_i - 1 : 0 + puts open(urls[idx]).read if urls[idx] + end + end + + desc 'lists all comments on a ticket and adds up the +1s' + task :comments do + setup_env + + if tic = get_ticket(ENV['ID']) + puts + puts 'Title : ' + tic.title + puts + puts '-- Comments --' + puts + + doc = Hpricot(open(get_xml_url(ENV['ID']))) + (doc/"versions > version").each do |comment| + #puts (comment/'updated-at').first.inner_html rescue nil + puts word_wrap((comment/:body).inner_html) rescue nil + puts '---' + end + + plus_ones = 0 + (doc/"versions > version > body").each do |comment| + if comment.inner_html.match(/\+1/) + plus_ones += 1 + end + end + puts "PLUSONES: " + plus_ones.to_s + end + end + + end + + + def comment_message +m = " +# Comment on the lighthouse ticket indicating your approval. Your comment +# should indicate that you like the change and what you like about it. +# Something like: +# +# '+1. I like the way you've restructured that code in generate_finder_sql, +# much nicer. The tests look good too.' +# +# If your comment simply says +1, then odds are other reviewers aren't +# going to take it too seriously. Show that you took the time to review +# the patch. " + end + + desc 'review a patch - comment and optionally +1 it' + task :comment do + setup_env + + if tic = get_ticket(ENV['ID']) + message_file = Tempfile.new('rails_message') + message_file.write comment_message() + message_file.close + + if ed_comment = get_editor_message(message_file.path) + tic.body = ed_comment.to_s + if tic.save + puts "Comment saved" + + doc = Hpricot(open(get_xml_url(ENV['ID']))) + plus_ones = 0 + (doc/"versions > version > body").each do |comment| + if comment.inner_html.match(/\+1/) + plus_ones += 1 + end + end + if plus_ones > 2 + # we have three, lets tag it + tic.tags << 'verified' + tic.save + end + end + end + end + end + + # possible future stuff : + # desc 'create a new branch and apply ticket patch (run tests?)' + # desc 'diff patched branch' + # desc 'rebase patched branch' + # desc 'create a new ticket with a patch' + +end + + +## LIGHTHOUSE API CLASS ## + +# Ruby lib for working with the Lighthouse API's XML interface. +# The first thing you need to set is the account name. This is the same +# as the web address for your account. +# +# Lighthouse.account = 'activereload' +# +# Then, you should set the authentication. You can either use your login +# credentials with HTTP Basic Authentication or with an API Tokens. You can +# find more info on tokens at http://lighthouseapp.com/help/using-beacons. +# +# # with basic authentication +# Lighthouse.authenticate('rick@techno-weenie.net', 'spacemonkey') +# +# # or, use a token +# Lighthouse.token = 'abcdefg' +# +# If no token or authentication info is given, you'll only be granted public access. +# +# This library is a small wrapper around the REST interface. You should read the docs at +# http://lighthouseapp.com/api. +# +module Lighthouse + class Error < StandardError; end + class << self + attr_accessor :email, :password, :host_format, :domain_format, :protocol, :port + attr_reader :account, :token + + # Sets the account name, and updates all the resources with the new domain. + def account=(name) + resources.each do |klass| + klass.site = klass.site_format % (host_format % [protocol, domain_format % name, port]) + end + @account = name + end + + # Sets up basic authentication credentials for all the resources. + def authenticate(email, password) + @email = email + @password = password + end + + # Sets the API token for all the resources. + def token=(value) + resources.each do |klass| + klass.headers['X-LighthouseToken'] = value + end + @token = value + end + + def resources + @resources ||= [] + end + end + + self.host_format = '%s://%s%s' + self.domain_format = '%s.lighthouseapp.com' + self.protocol = 'http' + self.port = '' + + class Base < ActiveResource::Base + def self.inherited(base) + Lighthouse.resources << base + class << base + attr_accessor :site_format + end + base.site_format = '%s' + super + end + end + + # Find projects + # + # Project.find(:all) # find all projects for the current account. + # Project.find(44) # find individual project by ID + # + # Creating a Project + # + # project = Project.new(:name => 'Ninja Whammy Jammy') + # project.save + # # => true + # + # Updating a Project + # + # project = Project.find(44) + # project.name = "Lighthouse Issues" + # project.public = false + # project.save + # + # Finding tickets + # + # project = Project.find(44) + # project.tickets + # + class Project < Base + def tickets(options = {}) + Ticket.find(:all, :params => options.update(:project_id => id)) + end + + def messages(options = {}) + Message.find(:all, :params => options.update(:project_id => id)) + end + + def milestones(options = {}) + Milestone.find(:all, :params => options.update(:project_id => id)) + end + + def bins(options = {}) + Bin.find(:all, :params => options.update(:project_id => id)) + end + end + + class User < Base + def memberships + Membership.find(:all, :params => {:user_id => id}) + end + end + + class Membership < Base + site_format << '/users/:user_id' + def save + raise Error, "Cannot modify Memberships from the API" + end + end + + class Token < Base + def save + raise Error, "Cannot modify Tokens from the API" + end + end + + # Find tickets + # + # Ticket.find(:all, :params => { :project_id => 44 }) + # Ticket.find(:all, :params => { :project_id => 44, :q => "state:closed tagged:committed" }) + # + # project = Project.find(44) + # project.tickets + # project.tickets(:q => "state:closed tagged:committed") + # + # Creating a Ticket + # + # ticket = Ticket.new(:project_id => 44) + # ticket.title = 'asdf' + # ... + # ticket.tags << 'ruby' << 'rails' << '@high' + # ticket.save + # + # Updating a Ticket + # + # ticket = Ticket.find(20, :params => { :project_id => 44 }) + # ticket.state = 'resolved' + # ticket.tags.delete '@high' + # ticket.save + # + class Ticket < Base + attr_writer :tags + site_format << '/projects/:project_id' + + def id + attributes['number'] ||= nil + number + end + + def tags + attributes['tag'] ||= nil + @tags ||= tag.blank? ? [] : parse_with_spaces(tag) + end + + def save_with_tags + self.tag = @tags.collect do |tag| + tag.include?(' ') ? tag.inspect : tag + end.join(" ") if @tags.is_a?(Array) + @tags = nil ; save_without_tags + end + + alias_method_chain :save, :tags + + private + # taken from Lighthouse Tag code + def parse_with_spaces(list) + tags = [] + + # first, pull out the quoted tags + list.gsub!(/\"(.*?)\"\s*/ ) { tags << $1; "" } + + # then, get whatever's left + tags.concat list.split(/\s/) + + cleanup_tags(tags) + end + + def cleanup_tags(tags) + returning tags do |tag| + tag.collect! do |t| + unless tag.blank? + t.downcase! + t.gsub! /(^')|('$)/, '' + t.gsub! /[^a-z0-9 \-_@\!']/, '' + t.strip! + t + end + end + tag.compact! + tag.uniq! + end + end + end + + class Message < Base + site_format << '/projects/:project_id' + end + + class Milestone < Base + site_format << '/projects/:project_id' + end + + class Bin < Base + site_format << '/projects/:project_id' + end +end + +module ActiveResource + class Connection + private + def authorization_header + (Lighthouse.email || Lighthouse.password ? { 'Authorization' => 'Basic ' + ["#{Lighthouse.email}:#{Lighthouse.password}"].pack('m').delete("\r\n") } : {}) + end + end +end + -- 1.5.4.3.484.g60e3