##// END OF EJS Templates
scm: use upcase to compare encoding name "UTF-8" in log converting....
Toshi MARUYAMA -
r5255:cae3fcce54bd
parent child
Show More
@@ -1,293 +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 153 @previous ||= Changeset.find(:first,
154 154 :conditions => ['id < ? AND repository_id = ?',
155 155 self.id, self.repository_id],
156 156 :order => 'id DESC')
157 157 end
158 158
159 159 # Returns the next changeset
160 160 def next
161 161 @next ||= Changeset.find(:first,
162 162 :conditions => ['id > ? AND repository_id = ?',
163 163 self.id, self.repository_id],
164 164 :order => 'id ASC')
165 165 end
166 166
167 167 # Creates a new Change from it's common parameters
168 168 def create_change(change)
169 169 Change.create(:changeset => self,
170 170 :action => change[:action],
171 171 :path => change[:path],
172 172 :from_path => change[:from_path],
173 173 :from_revision => change[:from_revision])
174 174 end
175 175
176 176 private
177 177
178 178 # Finds an issue that can be referenced by the commit message
179 179 # i.e. an issue that belong to the repository project, a subproject or a parent project
180 180 def find_referenced_issue_by_id(id)
181 181 return nil if id.blank?
182 182 issue = Issue.find_by_id(id.to_i, :include => :project)
183 183 if issue
184 184 unless issue.project &&
185 185 (project == issue.project || project.is_ancestor_of?(issue.project) ||
186 186 project.is_descendant_of?(issue.project))
187 187 issue = nil
188 188 end
189 189 end
190 190 issue
191 191 end
192 192
193 193 def fix_issue(issue)
194 194 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
195 195 if status.nil?
196 196 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
197 197 return issue
198 198 end
199 199
200 200 # the issue may have been updated by the closure of another one (eg. duplicate)
201 201 issue.reload
202 202 # don't change the status is the issue is closed
203 203 return if issue.status && issue.status.is_closed?
204 204
205 205 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
206 206 issue.status = status
207 207 unless Setting.commit_fix_done_ratio.blank?
208 208 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
209 209 end
210 210 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
211 211 { :changeset => self, :issue => issue })
212 212 unless issue.save
213 213 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
214 214 end
215 215 issue
216 216 end
217 217
218 218 def log_time(issue, hours)
219 219 time_entry = TimeEntry.new(
220 220 :user => user,
221 221 :hours => hours,
222 222 :issue => issue,
223 223 :spent_on => commit_date,
224 224 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
225 225 :locale => Setting.default_language)
226 226 )
227 227 time_entry.activity = log_time_activity unless log_time_activity.nil?
228 228
229 229 unless time_entry.save
230 230 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
231 231 end
232 232 time_entry
233 233 end
234 234
235 235 def log_time_activity
236 236 if Setting.commit_logtime_activity_id.to_i > 0
237 237 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
238 238 end
239 239 end
240 240
241 241 def split_comments
242 242 comments =~ /\A(.+?)\r?\n(.*)$/m
243 243 @short_comments = $1 || comments
244 244 @long_comments = $2.to_s.strip
245 245 return @short_comments, @long_comments
246 246 end
247 247
248 248 public
249 249
250 250 # Strips and reencodes a commit log before insertion into the database
251 251 def self.normalize_comments(str, encoding)
252 252 Changeset.to_utf8(str.to_s.strip, encoding)
253 253 end
254 254
255 255 private
256 256
257 257 def self.to_utf8(str, encoding)
258 258 return str if str.nil?
259 259 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
260 260 if str.empty?
261 261 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
262 262 return str
263 263 end
264 264 enc = encoding.blank? ? "UTF-8" : encoding
265 265 if str.respond_to?(:force_encoding)
266 if enc != "UTF-8"
266 if enc.upcase != "UTF-8"
267 267 str.force_encoding(enc)
268 268 str = str.encode("UTF-8", :invalid => :replace,
269 269 :undef => :replace, :replace => '?')
270 270 else
271 271 str.force_encoding("UTF-8")
272 272 if ! str.valid_encoding?
273 273 str = str.encode("US-ASCII", :invalid => :replace,
274 274 :undef => :replace, :replace => '?').encode("UTF-8")
275 275 end
276 276 end
277 277 else
278 278 ic = Iconv.new('UTF-8', enc)
279 279 txtar = ""
280 280 begin
281 281 txtar += ic.iconv(str)
282 282 rescue Iconv::IllegalSequence
283 283 txtar += $!.success
284 284 str = '?' + $!.failed[1,$!.failed.length]
285 285 retry
286 286 rescue
287 287 txtar += $!.success
288 288 end
289 289 str = txtar
290 290 end
291 291 str
292 292 end
293 293 end
General Comments 0
You need to be logged in to leave comments. Login now