require 'rubygems' require 'redminer' require 'yaml' module GitRedmine class MessageFormatError < RuntimeError; end class CommitLogError < RuntimeError; end class DiffError < RuntimeError; end class GitVersionError < RuntimeError; end module Config # host: Issue tracker host # port: Issue tracker port # default 80 # access_key: Issue tracker api access_key # verbose: Trace error option # default false CONFIG = YAML.load_file(File.dirname(__FILE__) + '/config.yml') rescue {} def host; CONFIG["host"] end def port; CONFIG["port"] || 80 end def access_key; CONFIG["access_key"] end def verbose; CONFIG["verbose"] end def redmine opts = {:port=>port, :verbose=>verbose, :reqtrace=>CONFIG["reqtrace"]} @redmine ||= Redminer::Base.new(host, access_key, opts) end def redmine_url ret = "http://#{host}" ret << ":#{port}" if port != 80 ret end end module GitCommands def backward_commit_hash(backward_number) `git log -#{backward_number} --format='%H'`.split("\n").last end def last_commit_log l = `git log -1 --format="%B"` raise GitVersionError.new('git version ~> 1.7.9') if l =~ /%B/ l end def diff_from(hash) `git diff #{hash}..HEAD --stat` end def origin_path remote = `git remote -v`.split("\n")[0] return if remote.nil? url = remote.split(" ")[1] url = url.gsub(/.*@/, '').gsub(/\.git$/, '').gsub(':', '/') url end end class CommitMessage include Config attr_accessor :issue_id, :message def issue_url "#{redmine_url}/issues/#{issue_id}" end end class InputCommitMessage < CommitMessage def initialize(commit_msg_file) @msg = "" File.open(commit_msg_file) do |f| while true begin @msg << f.readline rescue EOFError break end end end match = @msg.scan(/^@(\d+) (.*)/m) match = @msg.scan(/^(\d+) (.*)/m) if match.empty? match = @msg.scan(/^#{redmine_url}\/issues\/(\d+) (.*)/m) if match.empty? if match.empty? raise MessageFormatError.new("commit message format error\n\n#{@msg}") end msg = match[0][1] || "" msg = msg.strip[1..-1] if msg.strip.start_with?("\n") msg = msg.split(/diff --git/)[0] || "" @issue_id = match[0][0].to_i @message = msg end end class RemakeCommitMessage < CommitMessage include GitCommands attr_accessor :hash def initialize @hash = backward_commit_hash(2) res = last_commit_log raise CommitLogError.new if res.length < 3 self.parse(res) end def diff df = diff_from(self.hash) raise DiffError.new("cannot retrieve diff") if df.empty? df end # commit is a InputCommitMessage instance def self.remake(commit, issue) remake_msg = "@#{commit.issue_id} #{issue.subject}" unless commit.message.empty? remake_msg << "\n#{commit.message}" end remake_msg << "\n\n#{redmine_url}/issues/#{issue.id}" remake_msg end # res is a string of remade commit message def parse(res) res = res.split("\n") @issue_id = res.delete_at(0).scan(/^@(\d+) /)[0][0] rescue nil if @issue_id.nil? or @issue_id.empty? raise MessageFormatError.new("cannot find issue id\n\n#{res}") end @issue_id = issue_id.to_i issue_link = res.index { |ln| ln =~ /^http:\/\/#{host}/ } unless issue_link.nil? res.delete_at(issue_link) or (res[its-1].strip.empty? and res.delete_at(issue_link-1)) end @message = res end def origin_url path = origin_path return if path.nil? "http://#{path}/commit/#{hash}" end end module Hooks include GitCommands include Config def commit_msg(commit_msg_file) puts 'Hook: commit-msg' commit = InputCommitMessage.new(commit_msg_file) issue = redmine.issue(commit.issue_id) remake_msg = RemakeCommitMessage.remake(commit, issue) File.open(commit_msg_file, 'w+') { |f| f << remake_msg } ret = 0 rescue MessageFormatError => e error("Commit message format is not valid") puts "Commit message format:" puts " <issue id> <message>" puts "Example:" puts " 450 fix error" puts " #{redmine_url}/issues/450 fix error" ret = 1 rescue => e error(e.message) ret = 1 ensure puts e.backtrace.join("\n") if verbose and defined?(e) and not e.nil? ret end def post_commit puts 'Hook: post-commit' out = "Commit completed, " commit = RemakeCommitMessage.new issue = redmine.issue(commit.issue_id) diff = commit.diff diff = "<pre>#{diff}</pre>" commit_link = commit.origin_url commit_link = if commit_link "commit:\"#{commit.hash[0..6]}\":#{commit.origin_url}" else "commit:#{commit.hash[0..6]}" end note = "#{commit.message}\n\n#{commit_link}\n#{diff}" issue.update(note) ret = 0 rescue => e warn("#{out}, But error occurred... #{e.message}") ret = 1 ensure puts e.backtrace.join("\n") if verbose and defined?(e) and not e.nil? ret end def error(msg) puts "ERROR!\n\t#{msg}" puts "\tWanna skip verification? use --no-verify option\n" true end def warn(msg) puts "WARNING!\n\t#{msg}\n" true end end end