##// END OF EJS Templates
fix typo of fix_issue() at Changeset model...
Toshi MARUYAMA -
r7451:986f0acb00f3
parent child
Show More
@@ -1,301 +1,301
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'iconv'
18 require 'iconv'
19
19
20 class Changeset < ActiveRecord::Base
20 class Changeset < ActiveRecord::Base
21 belongs_to :repository
21 belongs_to :repository
22 belongs_to :user
22 belongs_to :user
23 has_many :changes, :dependent => :delete_all
23 has_many :changes, :dependent => :delete_all
24 has_and_belongs_to_many :issues
24 has_and_belongs_to_many :issues
25
25
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 :description => :long_comments,
27 :description => :long_comments,
28 :datetime => :committed_on,
28 :datetime => :committed_on,
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
30
30
31 acts_as_searchable :columns => 'comments',
31 acts_as_searchable :columns => 'comments',
32 :include => {:repository => :project},
32 :include => {:repository => :project},
33 :project_key => "#{Repository.table_name}.project_id",
33 :project_key => "#{Repository.table_name}.project_id",
34 :date_column => 'committed_on'
34 :date_column => 'committed_on'
35
35
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 :author_key => :user_id,
37 :author_key => :user_id,
38 :find_options => {:include => [:user, {:repository => :project}]}
38 :find_options => {:include => [:user, {:repository => :project}]}
39
39
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 validates_uniqueness_of :revision, :scope => :repository_id
41 validates_uniqueness_of :revision, :scope => :repository_id
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43
43
44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
45 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
46
46
47 after_create :scan_for_issues
47 after_create :scan_for_issues
48 before_create :before_create_cs
48 before_create :before_create_cs
49
49
50 def revision=(r)
50 def revision=(r)
51 write_attribute :revision, (r.nil? ? nil : r.to_s)
51 write_attribute :revision, (r.nil? ? nil : r.to_s)
52 end
52 end
53
53
54 # Returns the identifier of this changeset; depending on repository backends
54 # Returns the identifier of this changeset; depending on repository backends
55 def identifier
55 def identifier
56 if repository.class.respond_to? :changeset_identifier
56 if repository.class.respond_to? :changeset_identifier
57 repository.class.changeset_identifier self
57 repository.class.changeset_identifier self
58 else
58 else
59 revision.to_s
59 revision.to_s
60 end
60 end
61 end
61 end
62
62
63 def committed_on=(date)
63 def committed_on=(date)
64 self.commit_date = date
64 self.commit_date = date
65 super
65 super
66 end
66 end
67
67
68 # Returns the readable identifier
68 # Returns the readable identifier
69 def format_identifier
69 def format_identifier
70 if repository.class.respond_to? :format_changeset_identifier
70 if repository.class.respond_to? :format_changeset_identifier
71 repository.class.format_changeset_identifier self
71 repository.class.format_changeset_identifier self
72 else
72 else
73 identifier
73 identifier
74 end
74 end
75 end
75 end
76
76
77 def project
77 def project
78 repository.project
78 repository.project
79 end
79 end
80
80
81 def author
81 def author
82 user || committer.to_s.split('<').first
82 user || committer.to_s.split('<').first
83 end
83 end
84
84
85 def before_create_cs
85 def before_create_cs
86 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
86 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
87 self.comments = self.class.normalize_comments(
87 self.comments = self.class.normalize_comments(
88 self.comments, repository.repo_log_encoding)
88 self.comments, repository.repo_log_encoding)
89 self.user = repository.find_committer_user(self.committer)
89 self.user = repository.find_committer_user(self.committer)
90 end
90 end
91
91
92 def scan_for_issues
92 def scan_for_issues
93 scan_comment_for_issue_ids
93 scan_comment_for_issue_ids
94 end
94 end
95
95
96 TIMELOG_RE = /
96 TIMELOG_RE = /
97 (
97 (
98 ((\d+)(h|hours?))((\d+)(m|min)?)?
98 ((\d+)(h|hours?))((\d+)(m|min)?)?
99 |
99 |
100 ((\d+)(h|hours?|m|min))
100 ((\d+)(h|hours?|m|min))
101 |
101 |
102 (\d+):(\d+)
102 (\d+):(\d+)
103 |
103 |
104 (\d+([\.,]\d+)?)h?
104 (\d+([\.,]\d+)?)h?
105 )
105 )
106 /x
106 /x
107
107
108 def scan_comment_for_issue_ids
108 def scan_comment_for_issue_ids
109 return if comments.blank?
109 return if comments.blank?
110 # keywords used to reference issues
110 # keywords used to reference issues
111 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
111 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
112 ref_keywords_any = ref_keywords.delete('*')
112 ref_keywords_any = ref_keywords.delete('*')
113 # keywords used to fix issues
113 # keywords used to fix issues
114 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
114 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
115
115
116 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
116 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
117
117
118 referenced_issues = []
118 referenced_issues = []
119
119
120 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
120 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
121 action, refs = match[2], match[3]
121 action, refs = match[2], match[3]
122 next unless action.present? || ref_keywords_any
122 next unless action.present? || ref_keywords_any
123
123
124 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
124 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
125 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
125 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
126 if issue
126 if issue
127 referenced_issues << issue
127 referenced_issues << issue
128 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
128 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
129 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
129 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
130 end
130 end
131 end
131 end
132 end
132 end
133
133
134 referenced_issues.uniq!
134 referenced_issues.uniq!
135 self.issues = referenced_issues unless referenced_issues.empty?
135 self.issues = referenced_issues unless referenced_issues.empty?
136 end
136 end
137
137
138 def short_comments
138 def short_comments
139 @short_comments || split_comments.first
139 @short_comments || split_comments.first
140 end
140 end
141
141
142 def long_comments
142 def long_comments
143 @long_comments || split_comments.last
143 @long_comments || split_comments.last
144 end
144 end
145
145
146 def text_tag
146 def text_tag
147 if scmid?
147 if scmid?
148 "commit:#{scmid}"
148 "commit:#{scmid}"
149 else
149 else
150 "r#{revision}"
150 "r#{revision}"
151 end
151 end
152 end
152 end
153
153
154 # Returns the previous changeset
154 # Returns the previous changeset
155 def previous
155 def previous
156 @previous ||= Changeset.find(:first,
156 @previous ||= Changeset.find(:first,
157 :conditions => ['id < ? AND repository_id = ?',
157 :conditions => ['id < ? AND repository_id = ?',
158 self.id, self.repository_id],
158 self.id, self.repository_id],
159 :order => 'id DESC')
159 :order => 'id DESC')
160 end
160 end
161
161
162 # Returns the next changeset
162 # Returns the next changeset
163 def next
163 def next
164 @next ||= Changeset.find(:first,
164 @next ||= Changeset.find(:first,
165 :conditions => ['id > ? AND repository_id = ?',
165 :conditions => ['id > ? AND repository_id = ?',
166 self.id, self.repository_id],
166 self.id, self.repository_id],
167 :order => 'id ASC')
167 :order => 'id ASC')
168 end
168 end
169
169
170 # Creates a new Change from it's common parameters
170 # Creates a new Change from it's common parameters
171 def create_change(change)
171 def create_change(change)
172 Change.create(:changeset => self,
172 Change.create(:changeset => self,
173 :action => change[:action],
173 :action => change[:action],
174 :path => change[:path],
174 :path => change[:path],
175 :from_path => change[:from_path],
175 :from_path => change[:from_path],
176 :from_revision => change[:from_revision])
176 :from_revision => change[:from_revision])
177 end
177 end
178
178
179 private
179 private
180
180
181 # Finds an issue that can be referenced by the commit message
181 # Finds an issue that can be referenced by the commit message
182 # i.e. an issue that belong to the repository project, a subproject or a parent project
182 # i.e. an issue that belong to the repository project, a subproject or a parent project
183 def find_referenced_issue_by_id(id)
183 def find_referenced_issue_by_id(id)
184 return nil if id.blank?
184 return nil if id.blank?
185 issue = Issue.find_by_id(id.to_i, :include => :project)
185 issue = Issue.find_by_id(id.to_i, :include => :project)
186 if issue
186 if issue
187 unless issue.project &&
187 unless issue.project &&
188 (project == issue.project || project.is_ancestor_of?(issue.project) ||
188 (project == issue.project || project.is_ancestor_of?(issue.project) ||
189 project.is_descendant_of?(issue.project))
189 project.is_descendant_of?(issue.project))
190 issue = nil
190 issue = nil
191 end
191 end
192 end
192 end
193 issue
193 issue
194 end
194 end
195
195
196 def fix_issue(issue)
196 def fix_issue(issue)
197 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
197 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
198 if status.nil?
198 if status.nil?
199 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
199 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
200 return issue
200 return issue
201 end
201 end
202
202
203 # the issue may have been updated by the closure of another one (eg. duplicate)
203 # the issue may have been updated by the closure of another one (eg. duplicate)
204 issue.reload
204 issue.reload
205 # don't change the status is the issue is closed
205 # don't change the status is the issue is closed
206 return if issue.status && issue.status.is_closed?
206 return if issue.status && issue.status.is_closed?
207
207
208 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
208 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
209 issue.status = status
209 issue.status = status
210 unless Setting.commit_fix_done_ratio.blank?
210 unless Setting.commit_fix_done_ratio.blank?
211 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
211 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
212 end
212 end
213 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
213 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
214 { :changeset => self, :issue => issue })
214 { :changeset => self, :issue => issue })
215 unless issue.save
215 unless issue.save
216 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
216 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
217 end
217 end
218 issue
218 issue
219 end
219 end
220
220
221 def log_time(issue, hours)
221 def log_time(issue, hours)
222 time_entry = TimeEntry.new(
222 time_entry = TimeEntry.new(
223 :user => user,
223 :user => user,
224 :hours => hours,
224 :hours => hours,
225 :issue => issue,
225 :issue => issue,
226 :spent_on => commit_date,
226 :spent_on => commit_date,
227 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
227 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
228 :locale => Setting.default_language)
228 :locale => Setting.default_language)
229 )
229 )
230 time_entry.activity = log_time_activity unless log_time_activity.nil?
230 time_entry.activity = log_time_activity unless log_time_activity.nil?
231
231
232 unless time_entry.save
232 unless time_entry.save
233 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
233 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
234 end
234 end
235 time_entry
235 time_entry
236 end
236 end
237
237
238 def log_time_activity
238 def log_time_activity
239 if Setting.commit_logtime_activity_id.to_i > 0
239 if Setting.commit_logtime_activity_id.to_i > 0
240 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
240 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
241 end
241 end
242 end
242 end
243
243
244 def split_comments
244 def split_comments
245 comments =~ /\A(.+?)\r?\n(.*)$/m
245 comments =~ /\A(.+?)\r?\n(.*)$/m
246 @short_comments = $1 || comments
246 @short_comments = $1 || comments
247 @long_comments = $2.to_s.strip
247 @long_comments = $2.to_s.strip
248 return @short_comments, @long_comments
248 return @short_comments, @long_comments
249 end
249 end
250
250
251 public
251 public
252
252
253 # Strips and reencodes a commit log before insertion into the database
253 # Strips and reencodes a commit log before insertion into the database
254 def self.normalize_comments(str, encoding)
254 def self.normalize_comments(str, encoding)
255 Changeset.to_utf8(str.to_s.strip, encoding)
255 Changeset.to_utf8(str.to_s.strip, encoding)
256 end
256 end
257
257
258 def self.to_utf8(str, encoding)
258 def self.to_utf8(str, encoding)
259 return str if str.nil?
259 return str if str.nil?
260 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
260 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
261 if str.empty?
261 if str.empty?
262 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
262 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
263 return str
263 return str
264 end
264 end
265 enc = encoding.blank? ? "UTF-8" : encoding
265 enc = encoding.blank? ? "UTF-8" : encoding
266 if str.respond_to?(:force_encoding)
266 if str.respond_to?(:force_encoding)
267 if enc.upcase != "UTF-8"
267 if enc.upcase != "UTF-8"
268 str.force_encoding(enc)
268 str.force_encoding(enc)
269 str = str.encode("UTF-8", :invalid => :replace,
269 str = str.encode("UTF-8", :invalid => :replace,
270 :undef => :replace, :replace => '?')
270 :undef => :replace, :replace => '?')
271 else
271 else
272 str.force_encoding("UTF-8")
272 str.force_encoding("UTF-8")
273 if ! str.valid_encoding?
273 if ! str.valid_encoding?
274 str = str.encode("US-ASCII", :invalid => :replace,
274 str = str.encode("US-ASCII", :invalid => :replace,
275 :undef => :replace, :replace => '?').encode("UTF-8")
275 :undef => :replace, :replace => '?').encode("UTF-8")
276 end
276 end
277 end
277 end
278 elsif RUBY_PLATFORM == 'java'
278 elsif RUBY_PLATFORM == 'java'
279 begin
279 begin
280 ic = Iconv.new('UTF-8', enc)
280 ic = Iconv.new('UTF-8', enc)
281 str = ic.iconv(str)
281 str = ic.iconv(str)
282 rescue
282 rescue
283 str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
283 str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
284 end
284 end
285 else
285 else
286 ic = Iconv.new('UTF-8', enc)
286 ic = Iconv.new('UTF-8', enc)
287 txtar = ""
287 txtar = ""
288 begin
288 begin
289 txtar += ic.iconv(str)
289 txtar += ic.iconv(str)
290 rescue Iconv::IllegalSequence
290 rescue Iconv::IllegalSequence
291 txtar += $!.success
291 txtar += $!.success
292 str = '?' + $!.failed[1,$!.failed.length]
292 str = '?' + $!.failed[1,$!.failed.length]
293 retry
293 retry
294 rescue
294 rescue
295 txtar += $!.success
295 txtar += $!.success
296 end
296 end
297 str = txtar
297 str = txtar
298 end
298 end
299 str
299 str
300 end
300 end
301 end
301 end
General Comments 0
You need to be logged in to leave comments. Login now