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