#!/usr/bin/env ruby # A script to create a review on crucible based on the current git branch and # youtrack ticket. # # To configure settings, create a file named .code-review in your home directory # The format should be: # ------------------------------------------------------------------------------ # crucible: # url: # username: # password: # # # This section is optional, without it your ticket will not be automatically # # marked "In Review" # youtrack: # url: # username: # password: # # projects: # : # The name of the directory your project is in # key: # Project key found in crucible # repo: # Project repository found in crucible # reviewers: # The usernames of the reviewers # - # - require 'net/http' require 'json' require 'rexml/document' require 'yaml' require 'pathname' config = YAML.load_file(File.expand_path("~/.code-review")) project_name = nil Pathname.new(File.expand_path(".")).ascend do |path| if path.children.any? { |file| file.basename.to_s == ".git" } project_name = path.basename.to_s break end end unless project_name warn "Not a git repository (or any of the parent directories)" exit 1 end crucible = config['crucible'] youtrack = config['youtrack'] project = config['projects'][project_name] unless project warn "no configuation found for project: #{project_name}" exit 1 end crucible_url = crucible['url'] crucible_user_name = crucible['username'] crucible_password = crucible['password'] youtrack_url = youtrack['url'] if youtrack youtrack_user_name = youtrack['username'] if youtrack youtrack_password = youtrack['password'] if youtrack project_key = project['key'] repository = project['repo'] reviewers = project['reviewers'] # get info about the current branch from git display_name = `git config user.name` id = `git rev-parse --short HEAD`.strip branch = `git rev-parse --abbrev-ref HEAD`.strip commit_abbrev = `git log -1 --pretty=%B --abbrev-commit`.strip commit = `git log -1 --pretty=%B` def description(branch, commit, youtrack_url) res = commit res += "\n#{youtrack_url}/#{branch}" if youtrack_url res end def request(req) res = Net::HTTP.start(req.uri.host, req.uri.port) do |http| http.request req end unless res.is_a?(Net::HTTPSuccess) warn(if block_given? then yield(res) else res.body end) exit 1 end res end def request_json(req, crucible_user_name, crucible_password, &block) req.basic_auth crucible_user_name, crucible_password req["Content-Type"] = "application/json" req["Accept"] = "application/json" request(req) do |res| if block then yield(res) else JSON.parse(res.body)["message"] end end end def delete_review(crucible_url, review_id, crucible_user_name, crucible_password) # first we must move the review to abandoned uri = URI("#{crucible_url}/rest-service/reviews-v1/#{review_id}/transition?action=action:abandonReview") req = Net::HTTP::Post.new(uri) request_json(req, crucible_user_name, crucible_password) # now we delete the review uri = URI("#{crucible_url}/rest-service/reviews-v1/#{review_id}") req = Net::HTTP::Delete.new(uri) request_json(req, crucible_user_name, crucible_password) end # trap Ctrl-C so we delete the review if it has been created review_id = nil deleting = false trap("SIGINT") { break if deleting if review_id deleting = true puts "Deleting review..." delete_review(crucible_url, review_id, crucible_user_name, crucible_password) exit 0 else exit 1 end } # create a new review uri = URI("#{crucible_url}/rest-service/reviews-v1") req = Net::HTTP::Post.new(uri) req.body = { reviewData: { author: { displayName: display_name, userName: crucible_user_name }, name: "#{branch}: #{commit_abbrev}", description: description(branch, commit, youtrack_url), projectKey: project_key, state: "Review", allowReviewersToJoin: true } }.to_json res = request_json(req, crucible_user_name, crucible_password) review_id = JSON.parse(res.body)["permaId"]["id"] # now add the changeset (this request doesn't work with json for some reason) def add_changeset(crucible_url, review_id, repository, id, crucible_user_name, crucible_password) uri = URI("#{crucible_url}/rest-service/reviews-v1/#{review_id}/addChangeset") req = Net::HTTP::Post.new(uri) req.basic_auth crucible_user_name, crucible_password req["Content-Type"] = "application/xml" req["Accept"] = "application/xml" req.body = " #{repository} #{id} " res = Net::HTTP.start(req.uri.host, req.uri.port) do |http| http.request req end unless res.is_a?(Net::HTTPSuccess) error = REXML::Document.new(res.body).elements['error'] if error.elements['code'].first == 'IllegalArgument' warn "Failed to add changeset: #{id}, retrying..." sleep 3 # sleep a bit so we aren't pinging server too much add_changeset(crucible_url, review_id, repository, id, crucible_user_name, crucible_password) else error.elements['message'].first end end end add_changeset(crucible_url, review_id, repository, id, crucible_user_name, crucible_password) puts "Created review: #{crucible_url}/cru/#{review_id}" # add reviews to the review if they exist unless reviewers.empty? uri = URI("#{crucible_url}/rest-service/reviews-v1/#{review_id}/reviewers") req = Net::HTTP::Post.new(uri) req.body = reviewers.join(',') request_json(req, crucible_user_name, crucible_password) end if youtrack #login to youtrack uri = URI("#{youtrack_url}/rest/user/login") req = Net::HTTP::Post.new(uri) req.body = "login=#{youtrack_user_name}&password=#{youtrack_password}" res = request(req) cookie = res['Set-Cookie'] # update youtrack ticket status uri = URI("#{youtrack_url}/rest/issue/#{branch}/execute") req = Net::HTTP::Post.new(uri) req['Cookie'] = cookie req.body = "command=State%20In%20Review" request(req) do |res| REXML::Document.new(res.body).elements['error'].first end end