##// END OF EJS Templates
Rails3: replace deprecated 'before_create' to declared method at Changeset model....
Toshi MARUYAMA -
r6624:bc169cf466db
parent child
Show More
@@ -1,300 +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
49
49 def revision=(r)
50 def revision=(r)
50 write_attribute :revision, (r.nil? ? nil : r.to_s)
51 write_attribute :revision, (r.nil? ? nil : r.to_s)
51 end
52 end
52
53
53 # Returns the identifier of this changeset; depending on repository backends
54 # Returns the identifier of this changeset; depending on repository backends
54 def identifier
55 def identifier
55 if repository.class.respond_to? :changeset_identifier
56 if repository.class.respond_to? :changeset_identifier
56 repository.class.changeset_identifier self
57 repository.class.changeset_identifier self
57 else
58 else
58 revision.to_s
59 revision.to_s
59 end
60 end
60 end
61 end
61
62
62 def committed_on=(date)
63 def committed_on=(date)
63 self.commit_date = date
64 self.commit_date = date
64 super
65 super
65 end
66 end
66
67
67 # Returns the readable identifier
68 # Returns the readable identifier
68 def format_identifier
69 def format_identifier
69 if repository.class.respond_to? :format_changeset_identifier
70 if repository.class.respond_to? :format_changeset_identifier
70 repository.class.format_changeset_identifier self
71 repository.class.format_changeset_identifier self
71 else
72 else
72 identifier
73 identifier
73 end
74 end
74 end
75 end
75
76
76 def project
77 def project
77 repository.project
78 repository.project
78 end
79 end
79
80
80 def author
81 def author
81 user || committer.to_s.split('<').first
82 user || committer.to_s.split('<').first
82 end
83 end
83
84
84 def before_create
85 def before_create_cs
85 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)
86 self.comments = self.class.normalize_comments(
87 self.comments = self.class.normalize_comments(
87 self.comments, repository.repo_log_encoding)
88 self.comments, repository.repo_log_encoding)
88 self.user = repository.find_committer_user(self.committer)
89 self.user = repository.find_committer_user(self.committer)
89 end
90 end
90
91
91 def scan_for_issues
92 def scan_for_issues
92 scan_comment_for_issue_ids
93 scan_comment_for_issue_ids
93 end
94 end
94
95
95 TIMELOG_RE = /
96 TIMELOG_RE = /
96 (
97 (
97 ((\d+)(h|hours?))((\d+)(m|min)?)?
98 ((\d+)(h|hours?))((\d+)(m|min)?)?
98 |
99 |
99 ((\d+)(h|hours?|m|min))
100 ((\d+)(h|hours?|m|min))
100 |
101 |
101 (\d+):(\d+)
102 (\d+):(\d+)
102 |
103 |
103 (\d+([\.,]\d+)?)h?
104 (\d+([\.,]\d+)?)h?
104 )
105 )
105 /x
106 /x
106
107
107 def scan_comment_for_issue_ids
108 def scan_comment_for_issue_ids
108 return if comments.blank?
109 return if comments.blank?
109 # keywords used to reference issues
110 # keywords used to reference issues
110 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
111 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
111 ref_keywords_any = ref_keywords.delete('*')
112 ref_keywords_any = ref_keywords.delete('*')
112 # keywords used to fix issues
113 # keywords used to fix issues
113 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
114 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
114
115
115 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("|")
116
117
117 referenced_issues = []
118 referenced_issues = []
118
119
119 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|
120 action, refs = match[2], match[3]
121 action, refs = match[2], match[3]
121 next unless action.present? || ref_keywords_any
122 next unless action.present? || ref_keywords_any
122
123
123 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
124 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
124 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]
125 if issue
126 if issue
126 referenced_issues << issue
127 referenced_issues << issue
127 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
128 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
128 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
129 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
129 end
130 end
130 end
131 end
131 end
132 end
132
133
133 referenced_issues.uniq!
134 referenced_issues.uniq!
134 self.issues = referenced_issues unless referenced_issues.empty?
135 self.issues = referenced_issues unless referenced_issues.empty?
135 end
136 end
136
137
137 def short_comments
138 def short_comments
138 @short_comments || split_comments.first
139 @short_comments || split_comments.first
139 end
140 end
140
141
141 def long_comments
142 def long_comments
142 @long_comments || split_comments.last
143 @long_comments || split_comments.last
143 end
144 end
144
145
145 def text_tag
146 def text_tag
146 if scmid?
147 if scmid?
147 "commit:#{scmid}"
148 "commit:#{scmid}"
148 else
149 else
149 "r#{revision}"
150 "r#{revision}"
150 end
151 end
151 end
152 end
152
153
153 # Returns the previous changeset
154 # Returns the previous changeset
154 def previous
155 def previous
155 @previous ||= Changeset.find(:first,
156 @previous ||= Changeset.find(:first,
156 :conditions => ['id < ? AND repository_id = ?',
157 :conditions => ['id < ? AND repository_id = ?',
157 self.id, self.repository_id],
158 self.id, self.repository_id],
158 :order => 'id DESC')
159 :order => 'id DESC')
159 end
160 end
160
161
161 # Returns the next changeset
162 # Returns the next changeset
162 def next
163 def next
163 @next ||= Changeset.find(:first,
164 @next ||= Changeset.find(:first,
164 :conditions => ['id > ? AND repository_id = ?',
165 :conditions => ['id > ? AND repository_id = ?',
165 self.id, self.repository_id],
166 self.id, self.repository_id],
166 :order => 'id ASC')
167 :order => 'id ASC')
167 end
168 end
168
169
169 # Creates a new Change from it's common parameters
170 # Creates a new Change from it's common parameters
170 def create_change(change)
171 def create_change(change)
171 Change.create(:changeset => self,
172 Change.create(:changeset => self,
172 :action => change[:action],
173 :action => change[:action],
173 :path => change[:path],
174 :path => change[:path],
174 :from_path => change[:from_path],
175 :from_path => change[:from_path],
175 :from_revision => change[:from_revision])
176 :from_revision => change[:from_revision])
176 end
177 end
177
178
178 private
179 private
179
180
180 # Finds an issue that can be referenced by the commit message
181 # Finds an issue that can be referenced by the commit message
181 # 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
182 def find_referenced_issue_by_id(id)
183 def find_referenced_issue_by_id(id)
183 return nil if id.blank?
184 return nil if id.blank?
184 issue = Issue.find_by_id(id.to_i, :include => :project)
185 issue = Issue.find_by_id(id.to_i, :include => :project)
185 if issue
186 if issue
186 unless issue.project &&
187 unless issue.project &&
187 (project == issue.project || project.is_ancestor_of?(issue.project) ||
188 (project == issue.project || project.is_ancestor_of?(issue.project) ||
188 project.is_descendant_of?(issue.project))
189 project.is_descendant_of?(issue.project))
189 issue = nil
190 issue = nil
190 end
191 end
191 end
192 end
192 issue
193 issue
193 end
194 end
194
195
195 def fix_issue(issue)
196 def fix_issue(issue)
196 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)
197 if status.nil?
198 if status.nil?
198 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
199 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
199 return issue
200 return issue
200 end
201 end
201
202
202 # 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)
203 issue.reload
204 issue.reload
204 # don't change the status is the issue is closed
205 # don't change the status is the issue is closed
205 return if issue.status && issue.status.is_closed?
206 return if issue.status && issue.status.is_closed?
206
207
207 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))
208 issue.status = status
209 issue.status = status
209 unless Setting.commit_fix_done_ratio.blank?
210 unless Setting.commit_fix_done_ratio.blank?
210 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
211 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
211 end
212 end
212 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,
213 { :changeset => self, :issue => issue })
214 { :changeset => self, :issue => issue })
214 unless issue.save
215 unless issue.save
215 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
216 end
217 end
217 issue
218 issue
218 end
219 end
219
220
220 def log_time(issue, hours)
221 def log_time(issue, hours)
221 time_entry = TimeEntry.new(
222 time_entry = TimeEntry.new(
222 :user => user,
223 :user => user,
223 :hours => hours,
224 :hours => hours,
224 :issue => issue,
225 :issue => issue,
225 :spent_on => commit_date,
226 :spent_on => commit_date,
226 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
227 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
227 :locale => Setting.default_language)
228 :locale => Setting.default_language)
228 )
229 )
229 time_entry.activity = log_time_activity unless log_time_activity.nil?
230 time_entry.activity = log_time_activity unless log_time_activity.nil?
230
231
231 unless time_entry.save
232 unless time_entry.save
232 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
233 end
234 end
234 time_entry
235 time_entry
235 end
236 end
236
237
237 def log_time_activity
238 def log_time_activity
238 if Setting.commit_logtime_activity_id.to_i > 0
239 if Setting.commit_logtime_activity_id.to_i > 0
239 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
240 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
240 end
241 end
241 end
242 end
242
243
243 def split_comments
244 def split_comments
244 comments =~ /\A(.+?)\r?\n(.*)$/m
245 comments =~ /\A(.+?)\r?\n(.*)$/m
245 @short_comments = $1 || comments
246 @short_comments = $1 || comments
246 @long_comments = $2.to_s.strip
247 @long_comments = $2.to_s.strip
247 return @short_comments, @long_comments
248 return @short_comments, @long_comments
248 end
249 end
249
250
250 public
251 public
251
252
252 # Strips and reencodes a commit log before insertion into the database
253 # Strips and reencodes a commit log before insertion into the database
253 def self.normalize_comments(str, encoding)
254 def self.normalize_comments(str, encoding)
254 Changeset.to_utf8(str.to_s.strip, encoding)
255 Changeset.to_utf8(str.to_s.strip, encoding)
255 end
256 end
256
257
257 def self.to_utf8(str, encoding)
258 def self.to_utf8(str, encoding)
258 return str if str.nil?
259 return str if str.nil?
259 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
260 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
260 if str.empty?
261 if str.empty?
261 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
262 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
262 return str
263 return str
263 end
264 end
264 enc = encoding.blank? ? "UTF-8" : encoding
265 enc = encoding.blank? ? "UTF-8" : encoding
265 if str.respond_to?(:force_encoding)
266 if str.respond_to?(:force_encoding)
266 if enc.upcase != "UTF-8"
267 if enc.upcase != "UTF-8"
267 str.force_encoding(enc)
268 str.force_encoding(enc)
268 str = str.encode("UTF-8", :invalid => :replace,
269 str = str.encode("UTF-8", :invalid => :replace,
269 :undef => :replace, :replace => '?')
270 :undef => :replace, :replace => '?')
270 else
271 else
271 str.force_encoding("UTF-8")
272 str.force_encoding("UTF-8")
272 if ! str.valid_encoding?
273 if ! str.valid_encoding?
273 str = str.encode("US-ASCII", :invalid => :replace,
274 str = str.encode("US-ASCII", :invalid => :replace,
274 :undef => :replace, :replace => '?').encode("UTF-8")
275 :undef => :replace, :replace => '?').encode("UTF-8")
275 end
276 end
276 end
277 end
277 elsif RUBY_PLATFORM == 'java'
278 elsif RUBY_PLATFORM == 'java'
278 begin
279 begin
279 ic = Iconv.new('UTF-8', enc)
280 ic = Iconv.new('UTF-8', enc)
280 str = ic.iconv(str)
281 str = ic.iconv(str)
281 rescue
282 rescue
282 str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
283 str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
283 end
284 end
284 else
285 else
285 ic = Iconv.new('UTF-8', enc)
286 ic = Iconv.new('UTF-8', enc)
286 txtar = ""
287 txtar = ""
287 begin
288 begin
288 txtar += ic.iconv(str)
289 txtar += ic.iconv(str)
289 rescue Iconv::IllegalSequence
290 rescue Iconv::IllegalSequence
290 txtar += $!.success
291 txtar += $!.success
291 str = '?' + $!.failed[1,$!.failed.length]
292 str = '?' + $!.failed[1,$!.failed.length]
292 retry
293 retry
293 rescue
294 rescue
294 txtar += $!.success
295 txtar += $!.success
295 end
296 end
296 str = txtar
297 str = txtar
297 end
298 end
298 str
299 str
299 end
300 end
300 end
301 end
General Comments 0
You need to be logged in to leave comments. Login now