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