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