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