#!/usr/bin/env ruby #/ Usage: [options]... #/ Get info on pull requests from gazebo's bitbucket repository # based on http://www.alphadevx.com/a/88-Writing-a-REST-Client-in-Ruby # to install dependencies on Ubuntu (tested with Precise, Quantal, and Raring): #sudo apt-get install rubygems ruby-rest-client ruby-json begin require 'rubygems' require 'rest_client' require 'json' require 'optparse' require 'date' rescue LoadError => e puts "Error: " + e.message gem = e.message.match(/ -- (.*)/)[1] puts "Please install missing gem: $ gem install #{gem}" exit end $stderr.sync = true class BitbucketPullRequests # Pull request summary class Summary attr_reader :id attr_reader :source attr_reader :destination attr_reader :branch attr_reader :createdOn attr_reader :title def initialize(jsonHash, options) @options = options @id = jsonHash["id"] @source = " "*12 @destination = " "*12 @branch = "" @title = "" source = jsonHash["source"]["commit"] destination = jsonHash["destination"]["commit"] branch = jsonHash["source"]["branch"] title = jsonHash["title"] @source = source["hash"] if !source.nil? @destination = destination["hash"] if !destination.nil? @branch = branch["name"] if !branch.nil? @title = title if !title.nil? @createdOn = jsonHash["created_on"] end def to_s title = "" title += "\n" + @title + "\n" if @options["title"] title + @id.to_s.rjust(5, ' ') + " " + DateTime.parse(@createdOn).strftime("%Y-%m-%d") + " " + @source + " " + @destination + " " + @branch + "\n" end def date_and_string [@createdOn, self.to_s] end end # constructor def initialize(options) @url_pullrequests = 'https://bitbucket.org/api/2.0/repositories/osrf/gazebo/pullrequests' @options = options end # helpers for RestClient.get calls def getUrl(url) puts url if @options["show-url"] RestClient.get(url) end def getJson(url) json = JSON.parse(getUrl(url).body) if @options["verbose"] puts JSON.pretty_generate(json) end json end # summary of open pull requests def listPullRequests() jsonHash = getJson(@url_pullrequests + "/?state=OPEN") output = "" # Hash of pull requests. pullrequests = {} jsonHash["values"].each { |pr| date, str = Summary.new(pr, @options).date_and_string pullrequests[date] = str } while jsonHash.has_key? "next" jsonHash = getJson(jsonHash["next"]) jsonHash["values"].each { |pr| date, str = Summary.new(pr, @options).date_and_string pullrequests[date] = str } end # Generate output sorted by creation time pullrequests.keys.sort.each { |k| output += pullrequests[k] } return output end # summary of one pull request def getPullRequestSummary(id) jsonHash = getJson(@url_pullrequests + "/" + id.to_s) return Summary.new(jsonHash, @options) end ############################################### # Output a pull request summary based based on a branch and revision. # @param[in] _range A two part array, where the first is a branch name # and the second a revision. def getPullRequestSummaryFromRange(range) if range.nil? puts "Invalid range for --summary-range option." return elsif range.size < 2 puts "Error: --summary-range option requires two comma " + "separated arguments." return end origin = range[0] dest = range[1] # get the list of summaries from the log. Need to be cleaned up summaries_hg=`hg log -b #{origin} -P #{dest} | grep "(pull request.*)"` summaries_hg.split("\n").each { |sum| id = sum.scan(/#[0-9]*\)/).to_s()[/([0-9])+/] jsonHash = getJson(@url_pullrequests + "/" + id) puts "1. " + jsonHash["title"] puts " * [Pull request #{id}](https://bitbucket.org/osrf/gazebo/pull-request/#{id})" } end # diff of pull request def getPullRequestDiff(id) response = getUrl(@url_pullrequests + "/" + id.to_s + "/diff") puts response if @options["verbose"] return response end # list of files changed by pull request def getPullRequestFiles(id) getFilesFromDiff(getPullRequestDiff(id)) end # extract list of files added from diff string def getFilesFromDiff(diff) files = [] diff.lines.map(&:chomp).each do |line| if line.start_with? '+++ b/' line["+++ b/"] = "" # try to remove anything after a tab character # this is needed by --check 0 begin line[/\t.*$/] = "" # IndexError is raised if no tab characters are found rescue IndexError end files << line end end return files end # get ids for open pull requests def getOpenPullRequests() jsonHash = getJson(@url_pullrequests + "/?state=OPEN") ids = [] jsonHash["values"].each { |pr| ids << pr["id"].to_i } while jsonHash.has_key? "next" jsonHash = getJson(jsonHash["next"]) jsonHash["values"].each { |pr| ids << pr["id"].to_i } end return ids end # check files changed by the parent commit def checkCurrent() files = getFilesFromDiff(`hg log -r . --patch`) changeset = `hg id`[0..11] checkFiles(files, changeset) end # check changed files in pull request by id def checkPullRequest(id, fork=true) summary = getPullRequestSummary(id) puts "checking pull request #{id}, branch #{summary.branch}" files = getPullRequestFiles(id) `hg log -r #{summary.destination} 2>&1` if $? != 0 puts "Unknown revision #{summary.destination}, try: hg pull" return end `hg log -r #{summary.source} 2>&1` if $? != 0 puts "Unknown revision #{summary.source}, try: hg pull " + "(it could also be a fork)" return end ancestor=`hg log -r "ancestor(#{summary.source},#{summary.destination})" | head -1 | sed -e 's@.*:@@'`.chomp if ancestor != summary.destination puts "Need to merge branch #{summary.branch} with #{summary.destination}" end checkFiles(files, summary.source, fork) end def checkFiles(files, changeset, fork=true) files_list = "" files.each { |f| files_list += " " + f } hg_root = `hg root`.chomp if fork # this will allow real-time console output exec "echo #{files_list} | sh #{hg_root}/tools/code_check.sh --quick #{changeset}" else puts `echo #{files_list} | sh "#{hg_root}"/tools/code_check.sh --quick #{changeset}` end end end # default options options = {} options["list"] = false options["summary"] = nil options["check"] = false options["check_id"] = nil options["diff"] = nil options["files"] = nil options["range"] = nil options["show-url"] = false options["title"] = false options["verbose"] = false opt_parser = OptionParser.new do |o| o.on("-l", "--list", "List open pull requests with fields:\n" + " "*37 + "[id] [source] [dest] [branch]") { |o| options["list"] = o } o.on("-c", "--check [id]", Integer, "Run code_check on files changed by pull request [id]\n" + " "*37 + "if [id] is not supplied, check all open pull requests\n" + " "*37 + "if [id] is 0, check files changed by parent commit") { |o| options["check_id"] = o; options["check"] = true } o.on("-d", "--diff [id]", Integer, "Show diff from pull request") { |o| options["diff"] = o } o.on("-f", "--files [id]", Integer, "Show changed files in a pull request") { |o| options["files"] = o } o.on("--summary-range [changeset1],[changeset2]", Array, "Display all summaries from pull request\n\t\t\t\t\tmerged between changeset1 and changeset2") { |o| options["range"] = o } o.on("-s", "--summary [id]", Integer, "Summarize a pull request with fields:\n" + " "*37 + "[id] [source] [dest] [branch]") { |o| options["summary"] = o } o.on("-t", "--title", "Show pull request title with --list,\n\t\t\t\t\t--summary") { |o| options["title"] = o } o.on("-u", "--show-url", "Show urls accessed") { |o| options["show-url"] = o } o.on("-v", "--verbose", "Verbose output") { |o| options["verbose"] = o } o.on("-h", "--help", "Display this help message") do puts opt_parser exit end end opt_parser.parse! client = BitbucketPullRequests.new(options) if options["list"] puts client.listPullRequests() elsif !options["summary"].nil? puts client.getPullRequestSummary(options["summary"]) elsif !options["range"].nil? client.getPullRequestSummaryFromRange(options["range"]) elsif !options["diff"].nil? puts client.getPullRequestDiff(options["diff"]) elsif !options["files"].nil? puts client.getPullRequestFiles(options["files"]) elsif options["check"] if options["check_id"].nil? # check all open pull requests client.getOpenPullRequests().each { |id| client.checkPullRequest(id, false) } elsif options["check_id"] == 0 client.checkCurrent() else client.checkPullRequest(options["check_id"]) end else puts opt_parser end