Index: app/models/changeset.rb
===================================================================
--- app/models/changeset.rb (revision 1926)
+++ app/models/changeset.rb (working copy)
@@ -40,6 +40,8 @@
validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
+ after_create :parse_comment
+
def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s)
end
@@ -57,83 +59,221 @@
repository.project
end
- def after_create
- scan_comment_for_issue_ids
+ # This starts the comment parsing. Executed by an after_create filter
+ def parse_comment
+ return if comments.blank?
+
+ keywords = (ref_keywords + fix_keywords)
+ return if keywords.blank?
+
+ process_issues_marked_by(keywords)
end
- require 'pp'
-
- def scan_comment_for_issue_ids
- return if comments.blank?
- # keywords used to reference issues
- ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
- # keywords used to fix issues
- fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
- # status and optional done ratio applied
- fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
- done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
-
- kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
- return if kw_regexp.blank?
-
+
+ # Returns the previous changeset
+ def previous
+ @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
+ end
+
+ # Returns the next changeset
+ def next
+ @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
+ end
+
+ protected
+
+ # This parses the whole comment. Therefore the comment gets split into parts.
+ def process_issues_marked_by(ticket_keywords)
referenced_issues = []
-
- if ref_keywords.delete('*')
- # find any issue ID in the comments
- target_issue_ids = []
- comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
- referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
- end
-
- comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
+ comments.scan( splitting_regexp(ticket_keywords) ).each do |match|
action = match[0]
target_issue_ids = match[1].scan(/\d+/)
+ rest = match.last
+
target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
- if fix_status && fix_keywords.include?(action.downcase)
- # update status of issues
- logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
- target_issues.each do |issue|
- # the issue may have been updated by the closure of another one (eg. duplicate)
- issue.reload
- # don't change the status is the issue is closed
- next if issue.status.is_closed?
- user = committer_user || User.anonymous
- csettext = "r#{self.revision}"
- if self.scmid && (! (csettext =~ /^r[0-9]+$/))
- csettext = "commit:\"#{self.scmid}\""
- end
- journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
- issue.status = fix_status
- issue.done_ratio = done_ratio if done_ratio
- issue.save
- Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
- end
- end
+ process_part(action, target_issues, rest)
+
referenced_issues += target_issues
end
-
+
self.issues = referenced_issues.uniq
end
+ # returns a regexp that splits the long comment into parts
+ #
+ # Each part starts with a valid ticket reference and
+ # either ends with one or ends at the end of the comment
+ def splitting_regexp(ticket_keywords)
+ ref_any = ticket_keywords.delete('*')
+ joined_kw = ticket_keywords.join("|")
+ first = "(#{joined_kw})#{ref_any ? '*' : '+' }"
+ second = joined_kw + (ref_any ? '|#' : '')
+ /#{first}[\s:]*(([\s,;&]*#?\d+)+)(.*?)(?=#{second}|\Z)/im
+ end
+
+ # Process_part analyses the part and executes ticket changes, time logs etc.
+ def process_part(action,target_issues,rest)
+ # initialize three variables (time, ratio and timelogcomment) when advanced commit parsing is active
+ if Setting.advanced_commit_parsing?
+ time = extract_time!(rest)
+ ratio = extract_ratio!(rest)
+ timelogcomment = extract_timelogcomment!(rest)
+
+ # use changeset-id as timelog-comment if time is not nil (so timelog should be created) && no timelog-comment is given && Setting.commit_timelog_default_comment?
+ if !time.nil? && timelogcomment.nil? && Setting.commit_timelog_default_comment?
+ timelogcomment = "r#{self.revision}"
+ # use blank timelog-comment if time is not nil (so timelog should be created) && no timelog-comment is given && Setting.commit_timelog_default_comment isnt set
+ elsif !time.nil? && timelogcomment.nil?
+ timelogcomment = ""
+ end
+ end
+
+ target_issues.each do |issue|
+ if fix_status && action && fix_keywords.include?(action.downcase)
+ # create an issue-journal if the issue is closed due to fix_keywords
+ journal = init_journal(issue)
+ # add debug messages if issue is fixed
+ logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
+ # the issue may have been updated by the closure of another one (eg. duplicate)
+ issue.reload
+ # don't change the status if the issue is closed
+ break if issue.status.is_closed?
+ # update the issue-status and issue-done_ratio due to fix_keywords
+ issue.status = fix_status
+ issue.done_ratio = done_ratio if done_ratio
+ elsif action && !fix_keywords.include?(action.downcase) && ratio
+ # create an issue-journal if the issue is not updated due to fix_keywords && the issue is updated due to done_ratio_keywords (which requires Setting.advanced_commit_parsing active)
+ journal = init_journal_r(issue)
+ # the issue may have been updated by the closure of another one (eg. duplicate)
+ issue.reload
+ # don't change the done-ratio if the issue is closed
+ break if issue.status.is_closed?
+ # update the issue-done_ratio due to ratio_keywords
+ issue.done_ratio = ratio
+ elsif Setting.commit_ref_keywords == '*' && ratio
+ # create an issue-journal if ref_keywords equals '*' && the issue is updated due to done_ratio_keywords (which requires Setting.advanced_commit_parsing active)
+ journal = init_journal_r(issue)
+ # the issue may have been updated by the closure of another one (eg. duplicate)
+ issue.reload
+ # don't change the done-ratio if the issue is closed
+ break if issue.status.is_closed?
+ # update the issue-done_ratio due to ratio_keywords
+ issue.done_ratio = ratio
+ end
+
+ if time && issue.time_entries.find(:first, :conditions => ['spent_on = ? AND comments = ? AND user_id = ?',committed_on.to_date,timelogcomment[0..254],committer_user.id]).nil?
+ time_entry = TimeEntry.new( :hours => time,
+ :spent_on => committed_on.to_date,
+ :activity_id => activity_id,
+ :comments => timelogcomment[0..254],
+ :user => committer_user)
+ time_entry.hours /= target_issues.length
+ issue.time_entries << time_entry
+ end
+
+ if issue.changed?
+ issue.save
+ Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
+ end
+ end
+ end
+
+ # init the journal for our issue
+ def init_journal(issue)
+ csettext = "r#{self.revision}"
+ if self.scmid && (! (csettext =~ /^r[0-9]+$/))
+ csettext = "commit:\"#{self.scmid}\""
+ end
+ issue.init_journal(committer_user, l(:text_status_changed_by_changeset, csettext))
+ end
+
+ # init the journal for our issue when only the issue-done_ratio is changed
+ def init_journal_r(issue)
+ csettext = "r#{self.revision}"
+ if self.scmid && (! (csettext =~ /^r[0-9]+$/))
+ csettext = "commit:\"#{self.scmid}\""
+ end
+ issue.init_journal(committer_user, l(:text_ratio_changed_by_changeset, csettext))
+ end
+
+ # extracts the time
+ def extract_time!(string)
+ extract!(/(?:#{time_keywords.join("|")})[\s:]+(\d+[.,:hm ]*\d*[m ]*)/,string)
+ end
+
+ # extracts the ratio
+ def extract_ratio!(string)
+ extract!(/(?:#{ratio_keywords.join("|")})[\s:]+(\d+)%?/,string)
+ end
+
+ # extracts the timelogcomment
+ def extract_timelogcomment!(string)
+ extract!(/(?:#{timelogcomment_keywords.join("|")})[\s:]+(.*)?/,string)
+ end
+
+ # generic extract function. Notice the !. The original string is silently manipulated
+ def extract!(regexp,string)
+ if match = string.match(/(.*?)#{regexp}(.*)/mi)
+ replacement = if match[1] && !match[1].strip.empty?
+ match[1].strip + ' ' + match[3].strip
+ else
+ match[3].strip
+ end
+ string.replace(replacement)
+ match[2]
+ end
+ end
+
+ # keywords used to reference issues
+ def ref_keywords
+ @ref_keywords ||= Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
+ end
+
+ # keywords used to fix issues
+ def fix_keywords
+ @fix_keywords ||= Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
+ end
+
+ # keywords used to set the ratio of the issues
+ def ratio_keywords
+ @ratio_keywords ||= Setting.commit_ratio_keywords.downcase.split(',').collect(&:strip)
+ end
+
+ # keywords used to log time of an issue
+ def time_keywords
+ @time_keywords ||= Setting.commit_time_keywords.downcase.split(',').collect(&:strip)
+ end
+
+ # keywords used to set the comment of the timelog
+ def timelogcomment_keywords
+ @timelogcomment_keywords ||= Setting.commit_timelogcomment_keywords.downcase.split(',').collect(&:strip)
+ end
+
+ # status if an issue is fixed
+ def fix_status
+ @fix_status ||= IssueStatus.find_by_id(Setting.commit_fix_status_id)
+ end
+
+ # the ratio if an issue is fixed
+ def done_ratio
+ @done_ratio ||= Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
+ end
+
+ # gets the activity id for the timelog created via a commit-message from the global-settings
+ def activity_id
+ @activity_id ||= Setting.commit_timelog_activity_id
+ end
+
# Returns the Redmine User corresponding to the committer
+ # or the anonymous user
def committer_user
- if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
+ @user ||= if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3
u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank?
u
- end
+ end || User.anonymous
end
- # Returns the previous changeset
- def previous
- @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
- end
-
- # Returns the next changeset
- def next
- @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
- end
-
# Strips and reencodes a commit log before insertion into the database
def self.normalize_comments(str)
to_utf8(str.to_s.strip)
Index: app/models/repository.rb
===================================================================
--- app/models/repository.rb (revision 1926)
+++ app/models/repository.rb (working copy)
@@ -92,10 +92,6 @@
@latest_changeset ||= changesets.find(:first)
end
- def scan_changesets_for_issue_ids
- self.changesets.each(&:scan_comment_for_issue_ids)
- end
-
# fetch new changesets for all repositories
# can be called periodically by an external script
# eg. ruby script/runner "Repository.fetch_changesets"
@@ -103,11 +99,6 @@
find(:all).each(&:fetch_changesets)
end
- # scan changeset comments to find related and fixed issues for all repositories
- def self.scan_changesets_for_issue_ids
- find(:all).each(&:scan_changesets_for_issue_ids)
- end
-
def self.scm_name
'Abstract'
end
Index: app/views/settings/_repositories.rhtml
===================================================================
--- app/views/settings/_repositories.rhtml (revision 1926)
+++ app/views/settings/_repositories.rhtml (working copy)
@@ -32,5 +32,26 @@
<%= l(:text_comma_separated) %>