Mercurial > hg > soundsoftware-site
comparison app/models/changeset.rb @ 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 cbb26bc654de |
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 |