Feature #1518 » logtime_changeratio_via_commit-message_extended-r1926.diff
| app/models/changeset.rb (working copy) | ||
|---|---|---|
| 40 | 40 |
validates_uniqueness_of :revision, :scope => :repository_id |
| 41 | 41 |
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true |
| 42 | 42 |
|
| 43 |
after_create :parse_comment |
|
| 44 |
|
|
| 43 | 45 |
def revision=(r) |
| 44 | 46 |
write_attribute :revision, (r.nil? ? nil : r.to_s) |
| 45 | 47 |
end |
| ... | ... | |
| 57 | 59 |
repository.project |
| 58 | 60 |
end |
| 59 | 61 |
|
| 60 |
def after_create |
|
| 61 |
scan_comment_for_issue_ids |
|
| 62 |
# This starts the comment parsing. Executed by an after_create filter |
|
| 63 |
def parse_comment |
|
| 64 |
return if comments.blank? |
|
| 65 |
|
|
| 66 |
keywords = (ref_keywords + fix_keywords) |
|
| 67 |
return if keywords.blank? |
|
| 68 |
|
|
| 69 |
process_issues_marked_by(keywords) |
|
| 62 | 70 |
end |
| 63 |
require 'pp' |
|
| 64 |
|
|
| 65 |
def scan_comment_for_issue_ids
|
|
| 66 |
return if comments.blank?
|
|
| 67 |
# keywords used to reference issues
|
|
| 68 |
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
|
| 69 |
# keywords used to fix issues
|
|
| 70 |
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
|
|
| 71 |
# status and optional done ratio applied
|
|
| 72 |
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
|
|
| 73 |
done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i |
|
| 74 |
|
|
| 75 |
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
|
| 76 |
return if kw_regexp.blank?
|
|
| 77 |
|
|
| 71 |
|
|
| 72 |
# Returns the previous changeset |
|
| 73 |
def previous
|
|
| 74 |
@previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
|
|
| 75 |
end
|
|
| 76 |
|
|
| 77 |
# Returns the next changeset
|
|
| 78 |
def next
|
|
| 79 |
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
|
|
| 80 |
end
|
|
| 81 |
|
|
| 82 |
protected |
|
| 83 |
|
|
| 84 |
# This parses the whole comment. Therefore the comment gets split into parts.
|
|
| 85 |
def process_issues_marked_by(ticket_keywords) |
|
| 78 | 86 |
referenced_issues = [] |
| 79 |
|
|
| 80 |
if ref_keywords.delete('*')
|
|
| 81 |
# find any issue ID in the comments |
|
| 82 |
target_issue_ids = [] |
|
| 83 |
comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
|
|
| 84 |
referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids) |
|
| 85 |
end |
|
| 86 |
|
|
| 87 |
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
|
|
| 87 |
comments.scan( splitting_regexp(ticket_keywords) ).each do |match| |
|
| 88 | 88 |
action = match[0] |
| 89 | 89 |
target_issue_ids = match[1].scan(/\d+/) |
| 90 |
rest = match.last |
|
| 91 |
|
|
| 90 | 92 |
target_issues = repository.project.issues.find_all_by_id(target_issue_ids) |
| 91 |
if fix_status && fix_keywords.include?(action.downcase) |
|
| 92 |
# update status of issues |
|
| 93 |
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
|
|
| 94 |
target_issues.each do |issue| |
|
| 95 |
# the issue may have been updated by the closure of another one (eg. duplicate) |
|
| 96 |
issue.reload |
|
| 97 |
# don't change the status is the issue is closed |
|
| 98 |
next if issue.status.is_closed? |
|
| 99 |
user = committer_user || User.anonymous |
|
| 100 |
csettext = "r#{self.revision}"
|
|
| 101 |
if self.scmid && (! (csettext =~ /^r[0-9]+$/)) |
|
| 102 |
csettext = "commit:\"#{self.scmid}\""
|
|
| 103 |
end |
|
| 104 |
journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext)) |
|
| 105 |
issue.status = fix_status |
|
| 106 |
issue.done_ratio = done_ratio if done_ratio |
|
| 107 |
issue.save |
|
| 108 |
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
|
|
| 109 |
end |
|
| 110 |
end |
|
| 93 |
process_part(action, target_issues, rest) |
|
| 94 |
|
|
| 111 | 95 |
referenced_issues += target_issues |
| 112 | 96 |
end |
| 113 |
|
|
| 97 |
|
|
| 114 | 98 |
self.issues = referenced_issues.uniq |
| 115 | 99 |
end |
| 116 | 100 |
|
| 101 |
# returns a regexp that splits the long comment into parts |
|
| 102 |
# |
|
| 103 |
# Each part starts with a valid ticket reference and |
|
| 104 |
# either ends with one or ends at the end of the comment |
|
| 105 |
def splitting_regexp(ticket_keywords) |
|
| 106 |
ref_any = ticket_keywords.delete('*')
|
|
| 107 |
joined_kw = ticket_keywords.join("|")
|
|
| 108 |
first = "(#{joined_kw})#{ref_any ? '*' : '+' }"
|
|
| 109 |
second = joined_kw + (ref_any ? '|#' : '') |
|
| 110 |
/#{first}[\s:]*(([\s,;&]*#?\d+)+)(.*?)(?=#{second}|\Z)/im
|
|
| 111 |
end |
|
| 112 |
|
|
| 113 |
# Process_part analyses the part and executes ticket changes, time logs etc. |
|
| 114 |
def process_part(action,target_issues,rest) |
|
| 115 |
# initialize three variables (time, ratio and timelogcomment) when advanced commit parsing is active |
|
| 116 |
if Setting.advanced_commit_parsing? |
|
| 117 |
time = extract_time!(rest) |
|
| 118 |
ratio = extract_ratio!(rest) |
|
| 119 |
timelogcomment = extract_timelogcomment!(rest) |
|
| 120 |
|
|
| 121 |
# 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? |
|
| 122 |
if !time.nil? && timelogcomment.nil? && Setting.commit_timelog_default_comment? |
|
| 123 |
timelogcomment = "r#{self.revision}"
|
|
| 124 |
# 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 |
|
| 125 |
elsif !time.nil? && timelogcomment.nil? |
|
| 126 |
timelogcomment = "" |
|
| 127 |
end |
|
| 128 |
end |
|
| 129 |
|
|
| 130 |
target_issues.each do |issue| |
|
| 131 |
if fix_status && action && fix_keywords.include?(action.downcase) |
|
| 132 |
# create an issue-journal if the issue is closed due to fix_keywords |
|
| 133 |
journal = init_journal(issue) |
|
| 134 |
# add debug messages if issue is fixed |
|
| 135 |
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
|
|
| 136 |
# the issue may have been updated by the closure of another one (eg. duplicate) |
|
| 137 |
issue.reload |
|
| 138 |
# don't change the status if the issue is closed |
|
| 139 |
break if issue.status.is_closed? |
|
| 140 |
# update the issue-status and issue-done_ratio due to fix_keywords |
|
| 141 |
issue.status = fix_status |
|
| 142 |
issue.done_ratio = done_ratio if done_ratio |
|
| 143 |
elsif action && !fix_keywords.include?(action.downcase) && ratio |
|
| 144 |
# 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) |
|
| 145 |
journal = init_journal_r(issue) |
|
| 146 |
# the issue may have been updated by the closure of another one (eg. duplicate) |
|
| 147 |
issue.reload |
|
| 148 |
# don't change the done-ratio if the issue is closed |
|
| 149 |
break if issue.status.is_closed? |
|
| 150 |
# update the issue-done_ratio due to ratio_keywords |
|
| 151 |
issue.done_ratio = ratio |
|
| 152 |
elsif Setting.commit_ref_keywords == '*' && ratio |
|
| 153 |
# create an issue-journal if ref_keywords equals '*' && the issue is updated due to done_ratio_keywords (which requires Setting.advanced_commit_parsing active) |
|
| 154 |
journal = init_journal_r(issue) |
|
| 155 |
# the issue may have been updated by the closure of another one (eg. duplicate) |
|
| 156 |
issue.reload |
|
| 157 |
# don't change the done-ratio if the issue is closed |
|
| 158 |
break if issue.status.is_closed? |
|
| 159 |
# update the issue-done_ratio due to ratio_keywords |
|
| 160 |
issue.done_ratio = ratio |
|
| 161 |
end |
|
| 162 |
|
|
| 163 |
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? |
|
| 164 |
time_entry = TimeEntry.new( :hours => time, |
|
| 165 |
:spent_on => committed_on.to_date, |
|
| 166 |
:activity_id => activity_id, |
|
| 167 |
:comments => timelogcomment[0..254], |
|
| 168 |
:user => committer_user) |
|
| 169 |
time_entry.hours /= target_issues.length |
|
| 170 |
issue.time_entries << time_entry |
|
| 171 |
end |
|
| 172 |
|
|
| 173 |
if issue.changed? |
|
| 174 |
issue.save |
|
| 175 |
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
|
|
| 176 |
end |
|
| 177 |
end |
|
| 178 |
end |
|
| 179 |
|
|
| 180 |
# init the journal for our issue |
|
| 181 |
def init_journal(issue) |
|
| 182 |
csettext = "r#{self.revision}"
|
|
| 183 |
if self.scmid && (! (csettext =~ /^r[0-9]+$/)) |
|
| 184 |
csettext = "commit:\"#{self.scmid}\""
|
|
| 185 |
end |
|
| 186 |
issue.init_journal(committer_user, l(:text_status_changed_by_changeset, csettext)) |
|
| 187 |
end |
|
| 188 |
|
|
| 189 |
# init the journal for our issue when only the issue-done_ratio is changed |
|
| 190 |
def init_journal_r(issue) |
|
| 191 |
csettext = "r#{self.revision}"
|
|
| 192 |
if self.scmid && (! (csettext =~ /^r[0-9]+$/)) |
|
| 193 |
csettext = "commit:\"#{self.scmid}\""
|
|
| 194 |
end |
|
| 195 |
issue.init_journal(committer_user, l(:text_ratio_changed_by_changeset, csettext)) |
|
| 196 |
end |
|
| 197 |
|
|
| 198 |
# extracts the time |
|
| 199 |
def extract_time!(string) |
|
| 200 |
extract!(/(?:#{time_keywords.join("|")})[\s:]+(\d+[.,:hm ]*\d*[m ]*)/,string)
|
|
| 201 |
end |
|
| 202 |
|
|
| 203 |
# extracts the ratio |
|
| 204 |
def extract_ratio!(string) |
|
| 205 |
extract!(/(?:#{ratio_keywords.join("|")})[\s:]+(\d+)%?/,string)
|
|
| 206 |
end |
|
| 207 |
|
|
| 208 |
# extracts the timelogcomment |
|
| 209 |
def extract_timelogcomment!(string) |
|
| 210 |
extract!(/(?:#{timelogcomment_keywords.join("|")})[\s:]+(.*)?/,string)
|
|
| 211 |
end |
|
| 212 |
|
|
| 213 |
# generic extract function. Notice the !. The original string is silently manipulated |
|
| 214 |
def extract!(regexp,string) |
|
| 215 |
if match = string.match(/(.*?)#{regexp}(.*)/mi)
|
|
| 216 |
replacement = if match[1] && !match[1].strip.empty? |
|
| 217 |
match[1].strip + ' ' + match[3].strip |
|
| 218 |
else |
|
| 219 |
match[3].strip |
|
| 220 |
end |
|
| 221 |
string.replace(replacement) |
|
| 222 |
match[2] |
|
| 223 |
end |
|
| 224 |
end |
|
| 225 |
|
|
| 226 |
# keywords used to reference issues |
|
| 227 |
def ref_keywords |
|
| 228 |
@ref_keywords ||= Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
|
| 229 |
end |
|
| 230 |
|
|
| 231 |
# keywords used to fix issues |
|
| 232 |
def fix_keywords |
|
| 233 |
@fix_keywords ||= Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
|
|
| 234 |
end |
|
| 235 |
|
|
| 236 |
# keywords used to set the ratio of the issues |
|
| 237 |
def ratio_keywords |
|
| 238 |
@ratio_keywords ||= Setting.commit_ratio_keywords.downcase.split(',').collect(&:strip)
|
|
| 239 |
end |
|
| 240 |
|
|
| 241 |
# keywords used to log time of an issue |
|
| 242 |
def time_keywords |
|
| 243 |
@time_keywords ||= Setting.commit_time_keywords.downcase.split(',').collect(&:strip)
|
|
| 244 |
end |
|
| 245 |
|
|
| 246 |
# keywords used to set the comment of the timelog |
|
| 247 |
def timelogcomment_keywords |
|
| 248 |
@timelogcomment_keywords ||= Setting.commit_timelogcomment_keywords.downcase.split(',').collect(&:strip)
|
|
| 249 |
end |
|
| 250 |
|
|
| 251 |
# status if an issue is fixed |
|
| 252 |
def fix_status |
|
| 253 |
@fix_status ||= IssueStatus.find_by_id(Setting.commit_fix_status_id) |
|
| 254 |
end |
|
| 255 |
|
|
| 256 |
# the ratio if an issue is fixed |
|
| 257 |
def done_ratio |
|
| 258 |
@done_ratio ||= Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i |
|
| 259 |
end |
|
| 260 |
|
|
| 261 |
# gets the activity id for the timelog created via a commit-message from the global-settings |
|
| 262 |
def activity_id |
|
| 263 |
@activity_id ||= Setting.commit_timelog_activity_id |
|
| 264 |
end |
|
| 265 |
|
|
| 117 | 266 |
# Returns the Redmine User corresponding to the committer |
| 267 |
# or the anonymous user |
|
| 118 | 268 |
def committer_user |
| 119 |
if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/ |
|
| 269 |
@user ||= if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
|
|
| 120 | 270 |
username, email = $1.strip, $3 |
| 121 | 271 |
u = User.find_by_login(username) |
| 122 | 272 |
u ||= User.find_by_mail(email) unless email.blank? |
| 123 | 273 |
u |
| 124 |
end |
|
| 274 |
end || User.anonymous
|
|
| 125 | 275 |
end |
| 126 | 276 |
|
| 127 |
# Returns the previous changeset |
|
| 128 |
def previous |
|
| 129 |
@previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC') |
|
| 130 |
end |
|
| 131 |
|
|
| 132 |
# Returns the next changeset |
|
| 133 |
def next |
|
| 134 |
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC') |
|
| 135 |
end |
|
| 136 |
|
|
| 137 | 277 |
# Strips and reencodes a commit log before insertion into the database |
| 138 | 278 |
def self.normalize_comments(str) |
| 139 | 279 |
to_utf8(str.to_s.strip) |
| app/models/repository.rb (working copy) | ||
|---|---|---|
| 92 | 92 |
@latest_changeset ||= changesets.find(:first) |
| 93 | 93 |
end |
| 94 | 94 |
|
| 95 |
def scan_changesets_for_issue_ids |
|
| 96 |
self.changesets.each(&:scan_comment_for_issue_ids) |
|
| 97 |
end |
|
| 98 |
|
|
| 99 | 95 |
# fetch new changesets for all repositories |
| 100 | 96 |
# can be called periodically by an external script |
| 101 | 97 |
# eg. ruby script/runner "Repository.fetch_changesets" |
| ... | ... | |
| 103 | 99 |
find(:all).each(&:fetch_changesets) |
| 104 | 100 |
end |
| 105 | 101 |
|
| 106 |
# scan changeset comments to find related and fixed issues for all repositories |
|
| 107 |
def self.scan_changesets_for_issue_ids |
|
| 108 |
find(:all).each(&:scan_changesets_for_issue_ids) |
|
| 109 |
end |
|
| 110 |
|
|
| 111 | 102 |
def self.scm_name |
| 112 | 103 |
'Abstract' |
| 113 | 104 |
end |
| app/views/settings/_repositories.rhtml (working copy) | ||
|---|---|---|
| 32 | 32 |
<br /><em><%= l(:text_comma_separated) %></em></p> |
| 33 | 33 |
</fieldset> |
| 34 | 34 |
|
| 35 |
<fieldset class="box tabular settings"><legend><%= l(:text_issues_advanced_commit_message_keywords) %></legend> |
|
| 36 |
<p><label><%= l(:setting_advanced_commit_keywords) %></label> |
|
| 37 |
<%= check_box_tag 'settings[advanced_commit_parsing]', 1, Setting.advanced_commit_parsing?, :onclick=>"Element.toggle('advanced_keywords'); return true;" %><%= hidden_field_tag 'settings[advanced_commit_parsing]', 0 %></p>
|
|
| 38 |
|
|
| 39 |
<div id="advanced_keywords" <%= Setting.advanced_commit_parsing? ? '' : 'style="display:none"' %>> |
|
| 40 |
<p><label><%= l(:setting_commit_time_keywords) %></label> |
|
| 41 |
<%= text_field_tag 'settings[commit_time_keywords]', Setting.commit_time_keywords, :size => 30 %> |
|
| 42 |
<br /><em><%= l(:text_comma_separated) %></em></p> |
|
| 43 |
<p><label><%= l(:setting_commit_timelogcomment_keywords) %></label> |
|
| 44 |
<%= text_field_tag 'settings[commit_timelogcomment_keywords]', Setting.commit_timelogcomment_keywords, :size => 30 %> |
|
| 45 |
<br /><em><%= l(:text_comma_separated) %></em></p> |
|
| 46 |
<p><label><%= l(:setting_commit_timelog_activity_id) %></label> |
|
| 47 |
<%= select_tag 'settings[commit_timelog_activity_id]', options_for_select(Enumeration::get_values('ACTI').collect{|enumeration| [enumeration.name, enumeration.id.to_s]}, Setting.commit_timelog_activity_id) %></p>
|
|
| 48 |
<p><label><%= l(:setting_commit_timelog_default_comment) %></label> |
|
| 49 |
<%= check_box_tag 'settings[commit_timelog_default_comment]', 1, Setting.commit_timelog_default_comment? %><%= hidden_field_tag 'settings[commit_timelog_default_comment]', 0 %></p> |
|
| 50 |
<p><label><%= l(:setting_commit_ratio_keywords) %></label> |
|
| 51 |
<%= text_field_tag 'settings[commit_ratio_keywords]', Setting.commit_ratio_keywords, :size => 30 %> |
|
| 52 |
<br /><em><%= l(:text_comma_separated) %></em></p> |
|
| 53 |
</div> |
|
| 54 |
</fieldset> |
|
| 55 |
|
|
| 35 | 56 |
<%= submit_tag l(:button_save) %> |
| 36 | 57 |
<% end %> |
| config/settings.yml (working copy) | ||
|---|---|---|
| 81 | 81 |
default: 0 |
| 82 | 82 |
commit_fix_done_ratio: |
| 83 | 83 |
default: 100 |
| 84 |
advanced_commit_parsing: |
|
| 85 |
default: 0 |
|
| 86 |
commit_time_keywords: |
|
| 87 |
default: 'time,log' |
|
| 88 |
commit_ratio_keywords: |
|
| 89 |
default: 'done,ratio' |
|
| 90 |
commit_timelogcomment_keywords: |
|
| 91 |
default: 'timelogcomment' |
|
| 92 |
commit_timelog_activity_id: |
|
| 93 |
format: int |
|
| 94 |
default: 0 |
|
| 95 |
commit_timelog_default_comment: |
|
| 96 |
default: 0 |
|
| 84 | 97 |
# autologin duration in days |
| 85 | 98 |
# 0 means autologin is disabled |
| 86 | 99 |
autologin: |
| lang/en.yml (working copy) | ||
|---|---|---|
| 641 | 641 |
enumeration_issue_priorities: Issue priorities |
| 642 | 642 |
enumeration_doc_categories: Document categories |
| 643 | 643 |
enumeration_activities: Activities (time tracking) |
| 644 |
|
|
| 645 |
setting_advanced_commit_keywords: Enable advanced keywords |
|
| 646 |
setting_commit_time_keywords: Time logging keywords |
|
| 647 |
setting_commit_ratio_keywords: Done ratio keywords |
|
| 648 |
setting_commit_timelogcomment_keywords: Time logging comment keywords |
|
| 649 |
text_issues_advanced_commit_message_keywords: Logging time and setting issue ratios via commit messages |
|
| 650 |
setting_commit_timelog_activity_id: Default Activity-type for timelogs created via commit messages |
|
| 651 |
text_ratio_changed_by_changeset: Partially applied in changeset %s. |
|
| 652 |
setting_commit_timelog_default_comment: Use changeset-identifier if timelog comment is missing (else comment is blank) |
|
| test/unit/changeset_test.rb (working copy) | ||
|---|---|---|
| 32 | 32 |
c = Changeset.new(:repository => Project.find(1).repository, |
| 33 | 33 |
:committed_on => Time.now, |
| 34 | 34 |
:comments => 'New commit (#2). Fixes #1') |
| 35 |
c.scan_comment_for_issue_ids |
|
| 36 |
|
|
| 35 |
|
|
| 36 |
c.parse_comment |
|
| 37 |
|
|
| 37 | 38 |
assert_equal [1, 2], c.issue_ids.sort |
| 38 | 39 |
fixed = Issue.find(1) |
| 39 | 40 |
assert fixed.closed? |
| 40 | 41 |
assert_equal 90, fixed.done_ratio |
| 41 | 42 |
end |
| 43 |
|
|
| 44 |
def test_not_associate_any_mentioned_tickets |
|
| 45 |
Setting.commit_ref_keywords = 'key' |
|
| 46 |
|
|
| 47 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 48 |
:committed_on => Time.now, |
|
| 49 |
:comments => 'New commit (#2). #1') |
|
| 50 |
|
|
| 51 |
c.parse_comment |
|
| 52 |
|
|
| 53 |
assert_equal [], c.issue_ids |
|
| 54 |
end |
|
| 42 | 55 |
|
| 56 |
def test_fixes_multiple_tickets |
|
| 57 |
Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id |
|
| 58 |
Setting.commit_fix_keywords = 'fixes , closes' |
|
| 59 |
|
|
| 60 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 61 |
:committed_on => Time.now, |
|
| 62 |
:comments => 'Fixes #1,#2') |
|
| 63 |
|
|
| 64 |
c.parse_comment |
|
| 65 |
|
|
| 66 |
assert_equal [1, 2], c.issue_ids.sort |
|
| 67 |
end |
|
| 68 |
|
|
| 43 | 69 |
def test_ref_keywords_any_line_start |
| 44 | 70 |
Setting.commit_ref_keywords = '*' |
| 45 | 71 |
|
| 46 | 72 |
c = Changeset.new(:repository => Project.find(1).repository, |
| 47 | 73 |
:committed_on => Time.now, |
| 48 | 74 |
:comments => '#1 is the reason of this commit') |
| 49 |
c.scan_comment_for_issue_ids
|
|
| 75 |
c.parse_comment
|
|
| 50 | 76 |
|
| 51 | 77 |
assert_equal [1], c.issue_ids.sort |
| 52 | 78 |
end |
| 53 | 79 |
|
| 80 |
def test_log_time_without_issues_should_do_nothing |
|
| 81 |
count = TimeEntry.count |
|
| 82 |
Setting.advanced_commit_parsing = 1 |
|
| 83 |
Setting.commit_ref_keywords = '*' |
|
| 84 |
|
|
| 85 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 86 |
:committed_on => Time.now, |
|
| 87 |
:comments => 'time 3,5') |
|
| 88 |
c.parse_comment |
|
| 89 |
|
|
| 90 |
assert_equal count, TimeEntry.count |
|
| 91 |
end |
|
| 92 |
|
|
| 93 |
def test_log_time_should_work |
|
| 94 |
Setting.advanced_commit_parsing = 1 |
|
| 95 |
Setting.commit_ref_keywords = '*' |
|
| 96 |
|
|
| 97 |
comment = <<-EOL |
|
| 98 |
#1 |
|
| 99 |
time 3,5 |
|
| 100 |
EOL |
|
| 101 |
|
|
| 102 |
time = Time.now |
|
| 103 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 104 |
:committed_on => time, |
|
| 105 |
:comments => comment) |
|
| 106 |
|
|
| 107 |
c.parse_comment |
|
| 108 |
|
|
| 109 |
time_entry = TimeEntry.last |
|
| 110 |
assert_equal 3.5, time_entry.hours |
|
| 111 |
end |
|
| 112 |
|
|
| 113 |
def test_log_time_should_not_create_duplicate_logs |
|
| 114 |
count = TimeEntry.count |
|
| 115 |
Setting.advanced_commit_parsing = 1 |
|
| 116 |
Setting.commit_ref_keywords = '*' |
|
| 117 |
|
|
| 118 |
committed_on = Time.now |
|
| 119 |
|
|
| 120 |
comment = <<-EOL |
|
| 121 |
#1 |
|
| 122 |
time 3,5 |
|
| 123 |
EOL |
|
| 124 |
|
|
| 125 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 126 |
:committed_on => committed_on, |
|
| 127 |
:comments => comment) |
|
| 128 |
|
|
| 129 |
c.parse_comment |
|
| 130 |
c.parse_comment |
|
| 131 |
|
|
| 132 |
assert_equal count+1, TimeEntry.count |
|
| 133 |
end |
|
| 134 |
|
|
| 135 |
def test_log_time_splits_the_time_equally |
|
| 136 |
Setting.commit_fix_keywords = 'fixes , closes' |
|
| 137 |
Setting.advanced_commit_parsing = 1 |
|
| 138 |
|
|
| 139 |
comment = <<-EOL |
|
| 140 |
fixes #1,#2 |
|
| 141 |
time 3 |
|
| 142 |
timelogcomment the comment |
|
| 143 |
EOL |
|
| 144 |
|
|
| 145 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 146 |
:committed_on => Time.now, |
|
| 147 |
:comments => comment) |
|
| 148 |
|
|
| 149 |
c.parse_comment |
|
| 150 |
|
|
| 151 |
|
|
| 152 |
time_entry = TimeEntry.find(:first, :order => 'id DESC') |
|
| 153 |
assert_equal 1.5, time_entry.hours |
|
| 154 |
assert_equal 'the comment', time_entry.comments |
|
| 155 |
|
|
| 156 |
end |
|
| 157 |
|
|
| 158 |
def test_extract_time |
|
| 159 |
c = Changeset.new |
|
| 160 |
time_formats = [ "2", "21.1", "2,1","7:12", "10h", "10 h", "45m", "45 m", "3h15", "3h 15", "3 h 15", "3 h 15m", "3 h 15 m"] |
|
| 161 |
|
|
| 162 |
time_formats.each do |format| |
|
| 163 |
assert_equal format, c.send(:extract_time!, "time #{format}")
|
|
| 164 |
end |
|
| 165 |
end |
|
| 166 |
|
|
| 167 |
def test_set_done_ratio |
|
| 168 |
Setting.commit_ref_keywords = '*' |
|
| 169 |
Setting.advanced_commit_parsing = 1 |
|
| 170 |
|
|
| 171 |
comment = <<-EOL |
|
| 172 |
#1 |
|
| 173 |
done 50% |
|
| 174 |
#2 |
|
| 175 |
done 40 |
|
| 176 |
EOL |
|
| 177 |
|
|
| 178 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 179 |
:committed_on => Time.now, |
|
| 180 |
:comments => comment) |
|
| 181 |
|
|
| 182 |
c.parse_comment |
|
| 183 |
|
|
| 184 |
assert_equal [1, 2], c.issue_ids.sort |
|
| 185 |
assert_equal 50, Issue.find(1).done_ratio |
|
| 186 |
assert_equal 40, Issue.find(2).done_ratio |
|
| 187 |
end |
|
| 188 |
|
|
| 189 |
def test_set_committer_identified_by_email |
|
| 190 |
Setting.commit_fix_keywords = 'fixes' |
|
| 191 |
user = User.find(:first) |
|
| 192 |
|
|
| 193 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 194 |
:committed_on => Time.now, |
|
| 195 |
:committer => "arnie<#{user.mail}>",
|
|
| 196 |
:comments => 'Fixes #1') |
|
| 197 |
|
|
| 198 |
c.parse_comment |
|
| 199 |
|
|
| 200 |
fixed = Issue.find(1) |
|
| 201 |
assert fixed.closed? |
|
| 202 |
assert_equal user, fixed.journals.find(:first, :order => 'id DESC').user |
|
| 203 |
end |
|
| 204 |
|
|
| 205 |
def test_set_committer_identified_by_login |
|
| 206 |
Setting.commit_fix_keywords = 'fixes' |
|
| 207 |
user = User.find(:first) |
|
| 208 |
|
|
| 209 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 210 |
:committed_on => Time.now, |
|
| 211 |
:committer => user.login, |
|
| 212 |
:comments => 'Fixes #1') |
|
| 213 |
|
|
| 214 |
c.parse_comment |
|
| 215 |
|
|
| 216 |
fixed = Issue.find(1) |
|
| 217 |
assert fixed.closed? |
|
| 218 |
assert_equal user, fixed.journals.find(:first, :order => 'id DESC').user |
|
| 219 |
end |
|
| 220 |
|
|
| 221 |
def test_set_annonymous_if_committer_unknown |
|
| 222 |
Setting.commit_fix_keywords = 'fixes' |
|
| 223 |
|
|
| 224 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 225 |
:committed_on => Time.now, |
|
| 226 |
:committer => 'arnie', |
|
| 227 |
:comments => 'Fixes #1') |
|
| 228 |
|
|
| 229 |
c.parse_comment |
|
| 230 |
|
|
| 231 |
fixed = Issue.find(1) |
|
| 232 |
assert fixed.closed? |
|
| 233 |
assert_equal User.anonymous, fixed.journals.find(:first, :order => 'id DESC').user |
|
| 234 |
end |
|
| 235 |
|
|
| 236 |
def test_mail_deliveries |
|
| 237 |
ActionMailer::Base.deliveries.clear |
|
| 238 |
|
|
| 239 |
Setting.commit_fix_keywords = 'fixes' |
|
| 240 |
|
|
| 241 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 242 |
:committed_on => Time.now, |
|
| 243 |
:comments => 'Fixes #1') |
|
| 244 |
|
|
| 245 |
c.parse_comment |
|
| 246 |
|
|
| 247 |
assert_equal 1, ActionMailer::Base.deliveries.size |
|
| 248 |
end |
|
| 249 |
|
|
| 250 |
def test_ignore_cross_refrenced_issue_ids |
|
| 251 |
Setting.commit_fix_keywords = 'fixes' |
|
| 252 |
|
|
| 253 |
c = Changeset.new(:repository => Project.find(1).repository, |
|
| 254 |
:committed_on => Time.now, |
|
| 255 |
:comments => 'Fixes #1234') |
|
| 256 |
|
|
| 257 |
c.parse_comment |
|
| 258 |
|
|
| 259 |
assert_equal [], c.issue_ids.sort |
|
| 260 |
end |
|
| 261 |
|
|
| 54 | 262 |
def test_previous |
| 55 | 263 |
changeset = Changeset.find_by_revision('3')
|
| 56 | 264 |
assert_equal Changeset.find_by_revision('2'), changeset.previous
|
| ... | ... | |
| 70 | 278 |
changeset = Changeset.find_by_revision('4')
|
| 71 | 279 |
assert_nil changeset.next |
| 72 | 280 |
end |
| 281 |
|
|
| 282 |
def test_for_changeset_comments_strip |
|
| 283 |
comment = <<-COMMENT |
|
| 284 |
This is a loooooooooooooooooooooooooooong comment |
|
| 285 |
|
|
| 286 |
|
|
| 287 |
COMMENT |
|
| 288 |
changeset = Changeset.new :comments => comment |
|
| 289 |
assert_equal( 'This is a loooooooooooooooooooooooooooong comment', changeset.comments ) |
|
| 290 |
end |
|
| 291 |
|
|
| 292 |
|
|
| 73 | 293 |
end |
| test/unit/repository_test.rb (working copy) | ||
|---|---|---|
| 65 | 65 |
Setting.delete_all |
| 66 | 66 |
end |
| 67 | 67 |
|
| 68 |
def test_scan_changesets_for_issue_ids |
|
| 69 |
# choosing a status to apply to fix issues |
|
| 70 |
Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id |
|
| 71 |
Setting.commit_fix_done_ratio = "90" |
|
| 72 |
Setting.commit_ref_keywords = 'refs , references, IssueID' |
|
| 73 |
Setting.commit_fix_keywords = 'fixes , closes' |
|
| 74 |
Setting.default_language = 'en' |
|
| 75 |
ActionMailer::Base.deliveries.clear |
|
| 76 |
|
|
| 77 |
# make sure issue 1 is not already closed |
|
| 78 |
fixed_issue = Issue.find(1) |
|
| 79 |
assert !fixed_issue.status.is_closed? |
|
| 80 |
old_status = fixed_issue.status |
|
| 81 |
|
|
| 82 |
Repository.scan_changesets_for_issue_ids |
|
| 83 |
assert_equal [101, 102], Issue.find(3).changeset_ids |
|
| 84 |
|
|
| 85 |
# fixed issues |
|
| 86 |
fixed_issue.reload |
|
| 87 |
assert fixed_issue.status.is_closed? |
|
| 88 |
assert_equal 90, fixed_issue.done_ratio |
|
| 89 |
assert_equal [101], fixed_issue.changeset_ids |
|
| 90 |
|
|
| 91 |
# issue change |
|
| 92 |
journal = fixed_issue.journals.find(:first, :order => 'created_on desc') |
|
| 93 |
assert_equal User.find_by_login('dlopper'), journal.user
|
|
| 94 |
assert_equal 'Applied in changeset r2.', journal.notes |
|
| 95 |
|
|
| 96 |
# 2 email notifications |
|
| 97 |
assert_equal 2, ActionMailer::Base.deliveries.size |
|
| 98 |
mail = ActionMailer::Base.deliveries.first |
|
| 99 |
assert_kind_of TMail::Mail, mail |
|
| 100 |
assert mail.subject.starts_with?("[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
|
|
| 101 |
assert mail.body.include?("Status changed from #{old_status} to #{fixed_issue.status}")
|
|
| 102 |
|
|
| 103 |
# ignoring commits referencing an issue of another project |
|
| 104 |
assert_equal [], Issue.find(4).changesets |
|
| 105 |
end |
|
| 106 |
|
|
| 107 |
def test_for_changeset_comments_strip |
|
| 108 |
repository = Repository::Mercurial.create( :project => Project.find( 4 ), :url => '/foo/bar/baz' ) |
|
| 109 |
comment = <<-COMMENT |
|
| 110 |
This is a loooooooooooooooooooooooooooong comment |
|
| 111 |
|
|
| 112 |
|
|
| 113 |
COMMENT |
|
| 114 |
changeset = Changeset.new( |
|
| 115 |
:comments => comment, :commit_date => Time.now, :revision => 0, :scmid => 'f39b7922fb3c', |
|
| 116 |
:committer => 'foo <foo@example.com>', :committed_on => Time.now, :repository => repository ) |
|
| 117 |
assert( changeset.save ) |
|
| 118 |
assert_not_equal( comment, changeset.comments ) |
|
| 119 |
assert_equal( 'This is a loooooooooooooooooooooooooooong comment', changeset.comments ) |
|
| 120 |
end |
|
| 121 |
|
|
| 122 | 68 |
def test_for_urls_strip |
| 123 | 69 |
repository = Repository::Cvs.create(:project => Project.find(4), :url => ' :pserver:login:password@host:/path/to/the/repository', |
| 124 | 70 |
:root_url => 'foo ') |