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 ') |