require "ruby_git_hooks"
require "ruby_git_hooks/jira_ref_check"
require "rest-client"
require "json"
class JiraCommentAddHook < RubyGitHooks::Hook
Hook = RubyGitHooks::Hook
OPTIONS = [ "protocol", "host", "username", "password",
"api_path", "github", "issues",
"domain", "from", "subject", "via", "via_options", "intro", "conclusion",
"no_send", "check_status"]
VALID_ERROR_TYPES = [:no_jira, :invalid_jira]
attr_accessor :errors_to_report
def initialize(options = {})
bad_options = options.keys - OPTIONS
raise "JiraCommentAddHook created with unrecognized options: " +
"#{bad_options.inspect}!" if bad_options.size > 0
if !options.has_key?("username") || !options.has_key?("password")
raise "You must provide Jira server user name and password in options"
end
@options = options
@options["protocol"] ||= "https"
@options["host"] ||= "jira"
@options["api_path"] ||= "rest/api/latest/issue"
@options["github"] ||= "github.com"
@options["check_status"] = true if !@options.has_key? "check_status"
@options["domain"] ||= "mydomain.com"
@options["from"] ||= "Jira Jailer <noreply@#{@options["domain"]}>"
@options["subject"] ||= "Use Jira Ticket Numbers, Please!"
@options["via"] ||= "no_send"
@options["via_options"] ||= {}
@errors_to_report = {} end
def build_uri(ticket, command=nil)
uri = "#{@options['protocol']}://#{@options['username']}:#{@options['password']}@#{@options['host']}/#{@options['api_path']}/#{ticket}"
uri = "#{uri}/#{command}" if command
return uri
end
def check
success = true
commits.reverse_each do |commit|
commit_message = RubyGitHooks::Hook.shell!("git log #{commit} -1 --pretty=%B").rstrip
success = false unless check_one_commit(commit, commit_message )
end
report_errors
return success
end
def to_s
"<JiraCommentAddHook:#{object_id} #{@options.merge("password" => :redacted)}>"
end
def repo_remote_path
remote_urls = RubyGitHooks::Hook.shell!("git remote -v").split
remote = remote_urls[1] return "" if !remote
uri = URI.parse(remote) rescue nil
if uri
uri.to_s.sub(/.git\z/, "")
else
path = remote[/:([\w\/.-]*)/,1]
path.sub!(/.git\z/, "") if path
"#{@options['protocol']}://#{@options['github']}/#{path}"
end
end
def build_commit_uri(commit)
uri = "#{repo_remote_path}/commit/#{commit}"
end
def get_change_list(commit)
current, base = Hook.shell!("git log #{commit} --first-parent -2 --pretty=%H").split
if !base
files_with_status = Hook.shell!("git ls-tree --name-status -r #{commit}").split("\n")
files_with_status = files_with_status.map{|filename| "A\t" + filename}.join("\n")
else
files_with_status = Hook.shell!("git diff --name-status #{base}..#{current}")
end
files_with_status
end
def get_commit_branch(commit)
refs = self.commit_ref_map[commit]
refs ? refs.join(" ") : ""
end
def (commit, commit_message)
github_link = build_commit_uri(commit) changes = get_change_list(commit)
revision_and_date = Hook.shell!("git log #{commit} -1 --pretty='Revision: %h committed by %cn%nCommit date: %cd'") rescue ""
branch = "Branch: #{get_commit_branch(commit)}\n"
text = "#{revision_and_date}#{branch}#{github_link}\n\n#{commit_message}\n{noformat}\n#{changes}{noformat}"
end
def check_one_commit(commit, commit_message)
STDERR.puts "Checking #{commit[0..6]} #{commit_message.lines.first}"
jira_tickets = commit_message.scan(JiraReferenceCheckHook::JIRA_TICKET_REGEXP).map(&:strip).uniq
if jira_tickets.length == 0
STDERR.puts ">>Commit message must refer to a jira ticket"
add_error_to_report(commit, commit_message, "no_jira")
return false
end
= (commit, commit_message)
success = false
jira_tickets.each do |ticket|
valid_ticket = check_for_valid_ticket(ticket)
if valid_ticket
(ticket, )
success = true
end
end
unless success
STDERR.puts ">>Commit message must refer to a valid jira ticket"
add_error_to_report(commit, commit_message, "invalid_jira")
end
return success end
def (ticket, )
STDERR.puts "ADDING COMMENT for ticket #{ticket}"
uri = build_uri(ticket, "comment")
data = {"body" => }
STDERR.puts
if !@options["issues"] || @options["issues"].include?(ticket) resp = RestClient.post(uri, data.to_json, :content_type => :json, :accept=>:json)
end
end
def check_for_valid_ticket(ticket)
begin
uri = build_uri(ticket)
resp = RestClient.get uri
hash = JSON.parse(resp)
if @options["check_status"]
status = hash["fields"]["status"]["name"] rescue "open"
if status.downcase == "closed"
STDERR.puts "Issue #{ticket} is closed, not allowing."
return false
end
end
return true
rescue SocketError
STDERR.puts "SocketError finding '#{@options["host"]}': #{$!.inspect}"
STDERR.puts "Is '#{@options["host"]}' the right Jira hostname? "
STDERR.puts "I'm allowing this in case you're offline, but make sure"
STDERR.puts "your hostname is right, please!"
return true
rescue RestClient::Exception
if $!.http_code == 401
STDERR.puts "You're not authorized on this server!"
STDERR.puts "Please set your username and password correctly."
return false
elsif $!.http_code == 404
elsif $!.http_code == 407
STDERR.puts "We don't support proxies to Jira yet!"
STDERR.puts "I'll give you the benefit of the doubt."
return true
elsif $!.http_code >= 500
STDERR.puts "Jira got a server error."
STDERR.puts "I'll give you the benefit of the doubt."
return true
else
STDERR.puts "Unexpected HTTP Error: #{$!.http_code}!"
return false
end
rescue
STDERR.puts "Unexpected exception: #{$!.inspect}!"
return false
end
false end
def commit_list
return "" if !self.commits || self.commits.empty?
if self.commits.size == 1
"#{self.commits.first[0..6]}"
else
"#{self.commits.last[0..6]}..#{self.commits.first[0..6]}"
end
end
def add_error_to_report(commit, msg, error_type = "no_jira")
author_email = Hook.shell!("git log #{commit} -1 --pretty='%aN <%aE>'").chomp rescue "no email"
errors_to_report[author_email] ||= {"no_jira" => [], "invalid_jira" => []} errors_to_report[author_email][error_type] << "#{build_commit_uri(commit[0..7])}\n#{msg}"
end
def report_errors
require "pony" unless @options["no_send"] || @options["via"] == "no_send" errors_to_report.each do |email, details|
desc = build_message(details["no_jira"], details["invalid_jira"])
STDERR.puts "Warnings for commit from Jira Add Comment Check:\n--"
STDERR.puts "#{desc}\n--"
unless @options["no_send"] || @options["via"] == "no_send"
STDERR.puts "Sending warning email to #{email}"
ret = Pony.mail :to => email,
:from => @options["from"],
:subject => @options["subject"],
:body => desc,
:via => @options["via"],
:via_options => @options["via_options"]
end
end
end
def build_message(no_jira = [], invalid_jira= [])
description = @options["intro"] || ""
description.concat <<DESCRIPTION
This notice is to remind you that you need to include valid Jira ticket
numbers in all of your Git commits!
We encountered the following problems in your recent commits.
DESCRIPTION
if no_jira.size > 0
description.concat <<DESCRIPTION
Commits with no reference to any jira tickets:
#{no_jira.join("\n--\n ")}
-----
DESCRIPTION
end
if invalid_jira.size > 0
description.concat <<DESCRIPTION
Commits which reference invalid Jira ticket numbers
that don't exist or have already been closed:
#{invalid_jira.join("\n--\n ")}
-----
DESCRIPTION
end
description.concat @options["conclusion"] if @options["conclusion"]
description
end
end