comparison app/models/.svn/text-base/changeset.rb.svn-base @ 441:cbce1fd3b1b7 redmine-1.2

Update to Redmine 1.2-stable branch (Redmine SVN rev 6000)
author Chris Cannam
date Mon, 06 Jun 2011 14:24:13 +0100
parents 051f544170fe
children 753f1380d6bc
comparison
equal deleted inserted replaced
245:051f544170fe 441:cbce1fd3b1b7
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang 2 # Copyright (C) 2006-2011 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'
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.first || User.current, :view_changesets) } } 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
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))
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
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,
154 :conditions => ['id < ? AND repository_id = ?',
155 self.id, self.repository_id],
156 :order => 'id DESC')
153 end 157 end
154 158
155 # Returns the next changeset 159 # Returns the next changeset
156 def next 160 def next
157 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC') 161 @next ||= Changeset.find(:first,
158 end 162 :conditions => ['id > ? AND repository_id = ?',
159 163 self.id, self.repository_id],
164 :order => 'id ASC')
165 end
166
160 # Creates a new Change from it's common parameters 167 # Creates a new Change from it's common parameters
161 def create_change(change) 168 def create_change(change)
162 Change.create(:changeset => self, 169 Change.create(:changeset => self,
163 :action => change[:action], 170 :action => change[:action],
164 :path => change[:path], 171 :path => change[:path],
165 :from_path => change[:from_path], 172 :from_path => change[:from_path],
166 :from_revision => change[:from_revision]) 173 :from_revision => change[:from_revision])
167 end 174 end
168 175
169 private 176 private
170 177
172 # 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
173 def find_referenced_issue_by_id(id) 180 def find_referenced_issue_by_id(id)
174 return nil if id.blank? 181 return nil if id.blank?
175 issue = Issue.find_by_id(id.to_i, :include => :project) 182 issue = Issue.find_by_id(id.to_i, :include => :project)
176 if issue 183 if issue
177 unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project) 184 unless issue.project &&
185 (project == issue.project || project.is_ancestor_of?(issue.project) ||
186 project.is_descendant_of?(issue.project))
178 issue = nil 187 issue = nil
179 end 188 end
180 end 189 end
181 issue 190 issue
182 end 191 end
183 192
184 def fix_issue(issue) 193 def fix_issue(issue)
185 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)
186 if status.nil? 195 if status.nil?
187 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
188 return issue 197 return issue
189 end 198 end
190 199
191 # 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)
192 issue.reload 201 issue.reload
193 # don't change the status is the issue is closed 202 # don't change the status is the issue is closed
194 return if issue.status && issue.status.is_closed? 203 return if issue.status && issue.status.is_closed?
195 204
196 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))
197 issue.status = status 206 issue.status = status
198 unless Setting.commit_fix_done_ratio.blank? 207 unless Setting.commit_fix_done_ratio.blank?
199 issue.done_ratio = Setting.commit_fix_done_ratio.to_i 208 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
200 end 209 end
203 unless issue.save 212 unless issue.save
204 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
205 end 214 end
206 issue 215 issue
207 end 216 end
208 217
209 def log_time(issue, hours) 218 def log_time(issue, hours)
210 time_entry = TimeEntry.new( 219 time_entry = TimeEntry.new(
211 :user => user, 220 :user => user,
212 :hours => hours, 221 :hours => hours,
213 :issue => issue, 222 :issue => issue,
214 :spent_on => commit_date, 223 :spent_on => commit_date,
215 :comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language) 224 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
225 :locale => Setting.default_language)
216 ) 226 )
217 time_entry.activity = log_time_activity unless log_time_activity.nil? 227 time_entry.activity = log_time_activity unless log_time_activity.nil?
218 228
219 unless time_entry.save 229 unless time_entry.save
220 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
221 end 231 end
222 time_entry 232 time_entry
223 end 233 end
224 234
225 def log_time_activity 235 def log_time_activity
226 if Setting.commit_logtime_activity_id.to_i > 0 236 if Setting.commit_logtime_activity_id.to_i > 0
227 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i) 237 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
228 end 238 end
229 end 239 end
230 240
231 def split_comments 241 def split_comments
232 comments =~ /\A(.+?)\r?\n(.*)$/m 242 comments =~ /\A(.+?)\r?\n(.*)$/m
233 @short_comments = $1 || comments 243 @short_comments = $1 || comments
234 @long_comments = $2.to_s.strip 244 @long_comments = $2.to_s.strip
235 return @short_comments, @long_comments 245 return @short_comments, @long_comments
240 # Strips and reencodes a commit log before insertion into the database 250 # Strips and reencodes a commit log before insertion into the database
241 def self.normalize_comments(str, encoding) 251 def self.normalize_comments(str, encoding)
242 Changeset.to_utf8(str.to_s.strip, encoding) 252 Changeset.to_utf8(str.to_s.strip, encoding)
243 end 253 end
244 254
245 private
246
247 def self.to_utf8(str, encoding) 255 def self.to_utf8(str, encoding)
248 return str if str.blank? 256 return str if str.nil?
249 unless encoding.blank? || encoding == 'UTF-8' 257 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
258 if str.empty?
259 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
260 return str
261 end
262 enc = encoding.blank? ? "UTF-8" : encoding
263 if str.respond_to?(:force_encoding)
264 if enc.upcase != "UTF-8"
265 str.force_encoding(enc)
266 str = str.encode("UTF-8", :invalid => :replace,
267 :undef => :replace, :replace => '?')
268 else
269 str.force_encoding("UTF-8")
270 if ! str.valid_encoding?
271 str = str.encode("US-ASCII", :invalid => :replace,
272 :undef => :replace, :replace => '?').encode("UTF-8")
273 end
274 end
275 else
276 ic = Iconv.new('UTF-8', enc)
277 txtar = ""
250 begin 278 begin
251 str = Iconv.conv('UTF-8', encoding, str) 279 txtar += ic.iconv(str)
252 rescue Iconv::Failure 280 rescue Iconv::IllegalSequence
253 # do nothing here 281 txtar += $!.success
254 end 282 str = '?' + $!.failed[1,$!.failed.length]
255 end 283 retry
256 if str.respond_to?(:force_encoding) 284 rescue
257 str.force_encoding('UTF-8') 285 txtar += $!.success
258 if ! str.valid_encoding? 286 end
259 str = str.encode("US-ASCII", :invalid => :replace, 287 str = txtar
260 :undef => :replace, :replace => '?').encode("UTF-8")
261 end
262 else
263 # removes invalid UTF8 sequences
264 begin
265 str = Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
266 rescue Iconv::InvalidEncoding
267 # "UTF-8//IGNORE" is not supported on some OS
268 end
269 end 288 end
270 str 289 str
271 end 290 end
272 end 291 end