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