41 |
41 |
validates_uniqueness_of :revision, :scope => :repository_id
|
42 |
42 |
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
|
43 |
43 |
|
|
44 |
after_create :parse_comment
|
|
45 |
|
44 |
46 |
def revision=(r)
|
45 |
47 |
write_attribute :revision, (r.nil? ? nil : r.to_s)
|
46 |
48 |
end
|
... | ... | |
66 |
68 |
self.user = repository.find_committer_user(committer)
|
67 |
69 |
end
|
68 |
70 |
|
69 |
|
def after_create
|
70 |
|
scan_comment_for_issue_ids
|
|
71 |
# This starts the comment parsing. Executed by an after_create filter
|
|
72 |
def parse_comment
|
|
73 |
return if comments.blank?
|
|
74 |
|
|
75 |
keywords = (ref_keywords + fix_keywords)
|
|
76 |
return if keywords.blank?
|
|
77 |
|
|
78 |
process_issues_marked_by(keywords)
|
71 |
79 |
end
|
72 |
|
require 'pp'
|
73 |
|
|
74 |
|
def scan_comment_for_issue_ids
|
75 |
|
return if comments.blank?
|
76 |
|
# keywords used to reference issues
|
77 |
|
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
78 |
|
# keywords used to fix issues
|
79 |
|
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
|
80 |
|
# status and optional done ratio applied
|
81 |
|
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
|
82 |
|
done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
|
83 |
|
|
84 |
|
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
85 |
|
return if kw_regexp.blank?
|
86 |
|
|
|
80 |
|
|
81 |
# Returns the previous changeset
|
|
82 |
def previous
|
|
83 |
@previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
|
|
84 |
end
|
|
85 |
|
|
86 |
# Returns the next changeset
|
|
87 |
def next
|
|
88 |
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
|
|
89 |
end
|
|
90 |
|
|
91 |
protected
|
|
92 |
|
|
93 |
# This parses the whole comment. Therefore the comment gets split into parts.
|
|
94 |
def process_issues_marked_by(ticket_keywords)
|
87 |
95 |
referenced_issues = []
|
88 |
96 |
|
89 |
|
if ref_keywords.delete('*')
|
90 |
|
# find any issue ID in the comments
|
91 |
|
target_issue_ids = []
|
92 |
|
comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
|
93 |
|
referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
|
94 |
|
end
|
95 |
|
|
96 |
|
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
|
|
97 |
comments.scan( splitting_regexp(ticket_keywords) ).each do |match|
|
97 |
98 |
action = match[0]
|
98 |
99 |
target_issue_ids = match[1].scan(/\d+/)
|
|
100 |
rest = match.last
|
|
101 |
|
99 |
102 |
target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
|
100 |
|
if fix_status && fix_keywords.include?(action.downcase)
|
101 |
|
# update status of issues
|
102 |
|
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
|
103 |
|
target_issues.each do |issue|
|
104 |
|
# the issue may have been updated by the closure of another one (eg. duplicate)
|
105 |
|
issue.reload
|
106 |
|
# don't change the status is the issue is closed
|
107 |
|
next if issue.status.is_closed?
|
108 |
|
csettext = "r#{self.revision}"
|
109 |
|
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
|
110 |
|
csettext = "commit:\"#{self.scmid}\""
|
111 |
|
end
|
112 |
|
journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
|
113 |
|
issue.status = fix_status
|
114 |
|
issue.done_ratio = done_ratio if done_ratio
|
115 |
|
issue.save
|
116 |
|
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
|
117 |
|
end
|
118 |
|
end
|
|
103 |
process_part(action, target_issues, rest)
|
|
104 |
|
119 |
105 |
referenced_issues += target_issues
|
120 |
106 |
end
|
121 |
107 |
|
122 |
108 |
self.issues = referenced_issues.uniq
|
123 |
109 |
end
|
124 |
110 |
|
125 |
|
# Returns the previous changeset
|
126 |
|
def previous
|
127 |
|
@previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
|
|
111 |
# returns a regexp that splits the long comment into parts
|
|
112 |
#
|
|
113 |
# Each part starts with a valid ticket reference and
|
|
114 |
# either ends with one or ends at the end of the comment
|
|
115 |
def splitting_regexp(ticket_keywords)
|
|
116 |
ref_any = ticket_keywords.delete('*')
|
|
117 |
joined_kw = ticket_keywords.join("|")
|
|
118 |
first = "(#{joined_kw})#{ref_any ? '*' : '+' }"
|
|
119 |
second = joined_kw + (ref_any ? '|#' : '')
|
|
120 |
/#{first}[\s:]*(([\s,;&]*#?\d+)+)(.*?)(?=#{second}|\Z)/im
|
128 |
121 |
end
|
129 |
122 |
|
130 |
|
# Returns the next changeset
|
131 |
|
def next
|
132 |
|
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
|
|
123 |
# Process_part analyses the part and executes ticket changes, time logs etc.
|
|
124 |
def process_part(action,target_issues,rest)
|
|
125 |
# initialize three variables (time, ratio and timelogcomment) when advanced commit parsing is active
|
|
126 |
if Setting.advanced_commit_parsing?
|
|
127 |
time = extract_time!(rest)
|
|
128 |
ratio = extract_ratio!(rest)
|
|
129 |
timelogcomment = extract_timelogcomment!(rest)
|
|
130 |
|
|
131 |
# 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?
|
|
132 |
if !time.nil? && timelogcomment.nil? && Setting.commit_timelog_default_comment?
|
|
133 |
timelogcomment = "r#{self.revision}"
|
|
134 |
# 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
|
|
135 |
elsif !time.nil? && timelogcomment.nil?
|
|
136 |
timelogcomment = ""
|
|
137 |
end
|
|
138 |
end
|
|
139 |
|
|
140 |
target_issues.each do |issue|
|
|
141 |
if fix_status && action && fix_keywords.include?(action.downcase)
|
|
142 |
# create an issue-journal if the issue is closed due to fix_keywords
|
|
143 |
journal = init_journal(issue)
|
|
144 |
# add debug messages if issue is fixed
|
|
145 |
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
|
|
146 |
# the issue may have been updated by the closure of another one (eg. duplicate)
|
|
147 |
issue.reload
|
|
148 |
# don't change the status if the issue is closed
|
|
149 |
break if issue.status.is_closed?
|
|
150 |
# update the issue-status and issue-done_ratio due to fix_keywords
|
|
151 |
issue.status = fix_status
|
|
152 |
issue.done_ratio = done_ratio if done_ratio
|
|
153 |
elsif action && !fix_keywords.include?(action.downcase) && ratio
|
|
154 |
# 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)
|
|
155 |
journal = init_journal_r(issue)
|
|
156 |
# the issue may have been updated by the closure of another one (eg. duplicate)
|
|
157 |
issue.reload
|
|
158 |
# don't change the done-ratio if the issue is closed
|
|
159 |
break if issue.status.is_closed?
|
|
160 |
# update the issue-done_ratio due to ratio_keywords
|
|
161 |
issue.done_ratio = ratio
|
|
162 |
elsif Setting.commit_ref_keywords == '*' && ratio
|
|
163 |
# create an issue-journal if ref_keywords equals '*' && the issue is updated due to done_ratio_keywords (which requires Setting.advanced_commit_parsing active)
|
|
164 |
journal = init_journal_r(issue)
|
|
165 |
# the issue may have been updated by the closure of another one (eg. duplicate)
|
|
166 |
issue.reload
|
|
167 |
# don't change the done-ratio if the issue is closed
|
|
168 |
break if issue.status.is_closed?
|
|
169 |
# update the issue-done_ratio due to ratio_keywords
|
|
170 |
issue.done_ratio = ratio
|
|
171 |
end
|
|
172 |
|
|
173 |
if time && issue.time_entries.find(:first, :conditions => ['spent_on = ? AND comments = ? AND user_id = ?',committed_on.to_date,timelogcomment[0..254],user_id]).nil?
|
|
174 |
time_entry = TimeEntry.new( :hours => time,
|
|
175 |
:spent_on => committed_on.to_date,
|
|
176 |
:activity_id => activity_id,
|
|
177 |
:comments => timelogcomment[0..254],
|
|
178 |
:user => user || User.anonymous)
|
|
179 |
time_entry.hours /= target_issues.length
|
|
180 |
issue.time_entries << time_entry
|
|
181 |
end
|
|
182 |
|
|
183 |
if issue.changed?
|
|
184 |
issue.save
|
|
185 |
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
|
|
186 |
end
|
|
187 |
end
|
133 |
188 |
end
|
|
189 |
|
|
190 |
# init the journal for our issue
|
|
191 |
def init_journal(issue)
|
|
192 |
csettext = "r#{self.revision}"
|
|
193 |
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
|
|
194 |
csettext = "commit:\"#{self.scmid}\""
|
|
195 |
end
|
|
196 |
issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
|
|
197 |
end
|
|
198 |
|
|
199 |
# init the journal for our issue when only the issue-done_ratio is changed
|
|
200 |
def init_journal_r(issue)
|
|
201 |
csettext = "r#{self.revision}"
|
|
202 |
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
|
|
203 |
csettext = "commit:\"#{self.scmid}\""
|
|
204 |
end
|
|
205 |
issue.init_journal(user || User.anonymous, l(:text_ratio_changed_by_changeset, csettext))
|
|
206 |
end
|
|
207 |
|
|
208 |
# extracts the time
|
|
209 |
def extract_time!(string)
|
|
210 |
extract!(/(?:#{time_keywords.join("|")})[\s:]+(\d+[.,:hm ]*\d*[m ]*)/,string)
|
|
211 |
end
|
|
212 |
|
|
213 |
# extracts the ratio
|
|
214 |
def extract_ratio!(string)
|
|
215 |
extract!(/(?:#{ratio_keywords.join("|")})[\s:]+(\d+)%?/,string)
|
|
216 |
end
|
|
217 |
|
|
218 |
# extracts the timelogcomment
|
|
219 |
def extract_timelogcomment!(string)
|
|
220 |
extract!(/(?:#{timelogcomment_keywords.join("|")})[\s:]+(.*)?/,string)
|
|
221 |
end
|
134 |
222 |
|
|
223 |
# generic extract function. Notice the !. The original string is silently manipulated
|
|
224 |
def extract!(regexp,string)
|
|
225 |
if match = string.match(/(.*?)#{regexp}(.*)/mi)
|
|
226 |
replacement = if match[1] && !match[1].strip.empty?
|
|
227 |
match[1].strip + ' ' + match[3].strip
|
|
228 |
else
|
|
229 |
match[3].strip
|
|
230 |
end
|
|
231 |
string.replace(replacement)
|
|
232 |
match[2]
|
|
233 |
end
|
|
234 |
end
|
|
235 |
|
|
236 |
# keywords used to reference issues
|
|
237 |
def ref_keywords
|
|
238 |
@ref_keywords ||= Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
|
239 |
end
|
|
240 |
|
|
241 |
# keywords used to fix issues
|
|
242 |
def fix_keywords
|
|
243 |
@fix_keywords ||= Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
|
|
244 |
end
|
|
245 |
|
|
246 |
# keywords used to set the ratio of the issues
|
|
247 |
def ratio_keywords
|
|
248 |
@ratio_keywords ||= Setting.commit_ratio_keywords.downcase.split(',').collect(&:strip)
|
|
249 |
end
|
|
250 |
|
|
251 |
# keywords used to log time of an issue
|
|
252 |
def time_keywords
|
|
253 |
@time_keywords ||= Setting.commit_time_keywords.downcase.split(',').collect(&:strip)
|
|
254 |
end
|
|
255 |
|
|
256 |
# keywords used to set the comment of the timelog
|
|
257 |
def timelogcomment_keywords
|
|
258 |
@timelogcomment_keywords ||= Setting.commit_timelogcomment_keywords.downcase.split(',').collect(&:strip)
|
|
259 |
end
|
|
260 |
|
|
261 |
# status if an issue is fixed
|
|
262 |
def fix_status
|
|
263 |
@fix_status ||= IssueStatus.find_by_id(Setting.commit_fix_status_id)
|
|
264 |
end
|
|
265 |
|
|
266 |
# the ratio if an issue is fixed
|
|
267 |
def done_ratio
|
|
268 |
@done_ratio ||= Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
|
|
269 |
end
|
|
270 |
|
|
271 |
# gets the activity id for the timelog created via a commit-message from the global-settings
|
|
272 |
def activity_id
|
|
273 |
@activity_id ||= Setting.commit_timelog_activity_id
|
|
274 |
end
|
|
275 |
|
135 |
276 |
# Strips and reencodes a commit log before insertion into the database
|
136 |
277 |
def self.normalize_comments(str)
|
137 |
278 |
to_utf8(str.to_s.strip)
|