##// END OF EJS Templates
Don't link multiple changesets from the same commit multiple times (#17931)....
Jean-Philippe Lang -
r13063:a56754633520
parent child
Show More
@@ -1,283 +1,289
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 class Changeset < ActiveRecord::Base
19 19 belongs_to :repository
20 20 belongs_to :user
21 21 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
22 22 has_and_belongs_to_many :issues
23 23 has_and_belongs_to_many :parents,
24 24 :class_name => "Changeset",
25 25 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
26 26 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
27 27 has_and_belongs_to_many :children,
28 28 :class_name => "Changeset",
29 29 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
30 30 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
31 31
32 32 acts_as_event :title => Proc.new {|o| o.title},
33 33 :description => :long_comments,
34 34 :datetime => :committed_on,
35 35 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
36 36
37 37 acts_as_searchable :columns => 'comments',
38 38 :include => {:repository => :project},
39 39 :project_key => "#{Repository.table_name}.project_id",
40 40 :date_column => 'committed_on'
41 41
42 42 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
43 43 :author_key => :user_id,
44 44 :find_options => {:include => [:user, {:repository => :project}]}
45 45
46 46 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
47 47 validates_uniqueness_of :revision, :scope => :repository_id
48 48 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
49 49
50 50 scope :visible, lambda {|*args|
51 51 includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
52 52 }
53 53
54 54 after_create :scan_for_issues
55 55 before_create :before_create_cs
56 56
57 57 def revision=(r)
58 58 write_attribute :revision, (r.nil? ? nil : r.to_s)
59 59 end
60 60
61 61 # Returns the identifier of this changeset; depending on repository backends
62 62 def identifier
63 63 if repository.class.respond_to? :changeset_identifier
64 64 repository.class.changeset_identifier self
65 65 else
66 66 revision.to_s
67 67 end
68 68 end
69 69
70 70 def committed_on=(date)
71 71 self.commit_date = date
72 72 super
73 73 end
74 74
75 75 # Returns the readable identifier
76 76 def format_identifier
77 77 if repository.class.respond_to? :format_changeset_identifier
78 78 repository.class.format_changeset_identifier self
79 79 else
80 80 identifier
81 81 end
82 82 end
83 83
84 84 def project
85 85 repository.project
86 86 end
87 87
88 88 def author
89 89 user || committer.to_s.split('<').first
90 90 end
91 91
92 92 def before_create_cs
93 93 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
94 94 self.comments = self.class.normalize_comments(
95 95 self.comments, repository.repo_log_encoding)
96 96 self.user = repository.find_committer_user(self.committer)
97 97 end
98 98
99 99 def scan_for_issues
100 100 scan_comment_for_issue_ids
101 101 end
102 102
103 103 TIMELOG_RE = /
104 104 (
105 105 ((\d+)(h|hours?))((\d+)(m|min)?)?
106 106 |
107 107 ((\d+)(h|hours?|m|min))
108 108 |
109 109 (\d+):(\d+)
110 110 |
111 111 (\d+([\.,]\d+)?)h?
112 112 )
113 113 /x
114 114
115 115 def scan_comment_for_issue_ids
116 116 return if comments.blank?
117 117 # keywords used to reference issues
118 118 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
119 119 ref_keywords_any = ref_keywords.delete('*')
120 120 # keywords used to fix issues
121 121 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
122 122
123 123 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
124 124
125 125 referenced_issues = []
126 126
127 127 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
128 128 action, refs = match[2].to_s.downcase, match[3]
129 129 next unless action.present? || ref_keywords_any
130 130
131 131 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
132 132 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
133 if issue
133 if issue && !issue_linked_to_same_commit?(issue)
134 134 referenced_issues << issue
135 135 # Don't update issues or log time when importing old commits
136 136 unless repository.created_on && committed_on && committed_on < repository.created_on
137 137 fix_issue(issue, action) if fix_keywords.include?(action)
138 138 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
139 139 end
140 140 end
141 141 end
142 142 end
143 143
144 144 referenced_issues.uniq!
145 145 self.issues = referenced_issues unless referenced_issues.empty?
146 146 end
147 147
148 148 def short_comments
149 149 @short_comments || split_comments.first
150 150 end
151 151
152 152 def long_comments
153 153 @long_comments || split_comments.last
154 154 end
155 155
156 156 def text_tag(ref_project=nil)
157 157 repo = ""
158 158 if repository && repository.identifier.present?
159 159 repo = "#{repository.identifier}|"
160 160 end
161 161 tag = if scmid?
162 162 "commit:#{repo}#{scmid}"
163 163 else
164 164 "#{repo}r#{revision}"
165 165 end
166 166 if ref_project && project && ref_project != project
167 167 tag = "#{project.identifier}:#{tag}"
168 168 end
169 169 tag
170 170 end
171 171
172 172 # Returns the title used for the changeset in the activity/search results
173 173 def title
174 174 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
175 175 comm = short_comments.blank? ? '' : (': ' + short_comments)
176 176 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
177 177 end
178 178
179 179 # Returns the previous changeset
180 180 def previous
181 181 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
182 182 end
183 183
184 184 # Returns the next changeset
185 185 def next
186 186 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
187 187 end
188 188
189 189 # Creates a new Change from it's common parameters
190 190 def create_change(change)
191 191 Change.create(:changeset => self,
192 192 :action => change[:action],
193 193 :path => change[:path],
194 194 :from_path => change[:from_path],
195 195 :from_revision => change[:from_revision])
196 196 end
197 197
198 198 # Finds an issue that can be referenced by the commit message
199 199 def find_referenced_issue_by_id(id)
200 200 return nil if id.blank?
201 201 issue = Issue.includes(:project).where(:id => id.to_i).first
202 202 if Setting.commit_cross_project_ref?
203 203 # all issues can be referenced/fixed
204 204 elsif issue
205 205 # issue that belong to the repository project, a subproject or a parent project only
206 206 unless issue.project &&
207 207 (project == issue.project || project.is_ancestor_of?(issue.project) ||
208 208 project.is_descendant_of?(issue.project))
209 209 issue = nil
210 210 end
211 211 end
212 212 issue
213 213 end
214 214
215 215 private
216 216
217 # Returns true if the issue is already linked to the same commit
218 # from a different repository
219 def issue_linked_to_same_commit?(issue)
220 repository.same_commits_in_scope(issue.changesets, self).any?
221 end
222
217 223 # Updates the +issue+ according to +action+
218 224 def fix_issue(issue, action)
219 225 # the issue may have been updated by the closure of another one (eg. duplicate)
220 226 issue.reload
221 227 # don't change the status is the issue is closed
222 228 return if issue.status && issue.status.is_closed?
223 229
224 230 journal = issue.init_journal(user || User.anonymous,
225 231 ll(Setting.default_language,
226 232 :text_status_changed_by_changeset,
227 233 text_tag(issue.project)))
228 234 rule = Setting.commit_update_keywords_array.detect do |rule|
229 235 rule['keywords'].include?(action) &&
230 236 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
231 237 end
232 238 if rule
233 239 issue.assign_attributes rule.slice(*Issue.attribute_names)
234 240 end
235 241 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
236 242 { :changeset => self, :issue => issue, :action => action })
237 243 unless issue.save
238 244 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
239 245 end
240 246 issue
241 247 end
242 248
243 249 def log_time(issue, hours)
244 250 time_entry = TimeEntry.new(
245 251 :user => user,
246 252 :hours => hours,
247 253 :issue => issue,
248 254 :spent_on => commit_date,
249 255 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
250 256 :locale => Setting.default_language)
251 257 )
252 258 time_entry.activity = log_time_activity unless log_time_activity.nil?
253 259
254 260 unless time_entry.save
255 261 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
256 262 end
257 263 time_entry
258 264 end
259 265
260 266 def log_time_activity
261 267 if Setting.commit_logtime_activity_id.to_i > 0
262 268 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
263 269 end
264 270 end
265 271
266 272 def split_comments
267 273 comments =~ /\A(.+?)\r?\n(.*)$/m
268 274 @short_comments = $1 || comments
269 275 @long_comments = $2.to_s.strip
270 276 return @short_comments, @long_comments
271 277 end
272 278
273 279 public
274 280
275 281 # Strips and reencodes a commit log before insertion into the database
276 282 def self.normalize_comments(str, encoding)
277 283 Changeset.to_utf8(str.to_s.strip, encoding)
278 284 end
279 285
280 286 def self.to_utf8(str, encoding)
281 287 Redmine::CodesetUtil.to_utf8(str, encoding)
282 288 end
283 289 end
@@ -1,484 +1,496
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 class ScmFetchError < Exception; end
19 19
20 20 class Repository < ActiveRecord::Base
21 21 include Redmine::Ciphering
22 22 include Redmine::SafeAttributes
23 23
24 24 # Maximum length for repository identifiers
25 25 IDENTIFIER_MAX_LENGTH = 255
26 26
27 27 belongs_to :project
28 28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29 29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30 30
31 31 serialize :extra_info
32 32
33 33 before_save :check_default
34 34
35 35 # Raw SQL to delete changesets and changes in the database
36 36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 37 before_destroy :clear_changesets
38 38
39 39 validates_length_of :password, :maximum => 255, :allow_nil => true
40 40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
44 44 # donwcase letters, digits, dashes, underscores but not digits only
45 45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
46 46 # Checks if the SCM is enabled when creating a repository
47 47 validate :repo_create_validation, :on => :create
48 48
49 49 safe_attributes 'identifier',
50 50 'login',
51 51 'password',
52 52 'path_encoding',
53 53 'log_encoding',
54 54 'is_default'
55 55
56 56 safe_attributes 'url',
57 57 :if => lambda {|repository, user| repository.new_record?}
58 58
59 59 def repo_create_validation
60 60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 61 errors.add(:type, :invalid)
62 62 end
63 63 end
64 64
65 65 def self.human_attribute_name(attribute_key_name, *args)
66 66 attr_name = attribute_key_name.to_s
67 67 if attr_name == "log_encoding"
68 68 attr_name = "commit_logs_encoding"
69 69 end
70 70 super(attr_name, *args)
71 71 end
72 72
73 73 # Removes leading and trailing whitespace
74 74 def url=(arg)
75 75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 76 end
77 77
78 78 # Removes leading and trailing whitespace
79 79 def root_url=(arg)
80 80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 81 end
82 82
83 83 def password
84 84 read_ciphered_attribute(:password)
85 85 end
86 86
87 87 def password=(arg)
88 88 write_ciphered_attribute(:password, arg)
89 89 end
90 90
91 91 def scm_adapter
92 92 self.class.scm_adapter_class
93 93 end
94 94
95 95 def scm
96 96 unless @scm
97 97 @scm = self.scm_adapter.new(url, root_url,
98 98 login, password, path_encoding)
99 99 if root_url.blank? && @scm.root_url.present?
100 100 update_attribute(:root_url, @scm.root_url)
101 101 end
102 102 end
103 103 @scm
104 104 end
105 105
106 106 def scm_name
107 107 self.class.scm_name
108 108 end
109 109
110 110 def name
111 111 if identifier.present?
112 112 identifier
113 113 elsif is_default?
114 114 l(:field_repository_is_default)
115 115 else
116 116 scm_name
117 117 end
118 118 end
119 119
120 120 def identifier=(identifier)
121 121 super unless identifier_frozen?
122 122 end
123 123
124 124 def identifier_frozen?
125 125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 126 end
127 127
128 128 def identifier_param
129 129 if is_default?
130 130 nil
131 131 elsif identifier.present?
132 132 identifier
133 133 else
134 134 id.to_s
135 135 end
136 136 end
137 137
138 138 def <=>(repository)
139 139 if is_default?
140 140 -1
141 141 elsif repository.is_default?
142 142 1
143 143 else
144 144 identifier.to_s <=> repository.identifier.to_s
145 145 end
146 146 end
147 147
148 148 def self.find_by_identifier_param(param)
149 149 if param.to_s =~ /^\d+$/
150 150 find_by_id(param)
151 151 else
152 152 find_by_identifier(param)
153 153 end
154 154 end
155 155
156 156 # TODO: should return an empty hash instead of nil to avoid many ||{}
157 157 def extra_info
158 158 h = read_attribute(:extra_info)
159 159 h.is_a?(Hash) ? h : nil
160 160 end
161 161
162 162 def merge_extra_info(arg)
163 163 h = extra_info || {}
164 164 return h if arg.nil?
165 165 h.merge!(arg)
166 166 write_attribute(:extra_info, h)
167 167 end
168 168
169 169 def report_last_commit
170 170 true
171 171 end
172 172
173 173 def supports_cat?
174 174 scm.supports_cat?
175 175 end
176 176
177 177 def supports_annotate?
178 178 scm.supports_annotate?
179 179 end
180 180
181 181 def supports_all_revisions?
182 182 true
183 183 end
184 184
185 185 def supports_directory_revisions?
186 186 false
187 187 end
188 188
189 189 def supports_revision_graph?
190 190 false
191 191 end
192 192
193 193 def entry(path=nil, identifier=nil)
194 194 scm.entry(path, identifier)
195 195 end
196 196
197 197 def scm_entries(path=nil, identifier=nil)
198 198 scm.entries(path, identifier)
199 199 end
200 200 protected :scm_entries
201 201
202 202 def entries(path=nil, identifier=nil)
203 203 entries = scm_entries(path, identifier)
204 204 load_entries_changesets(entries)
205 205 entries
206 206 end
207 207
208 208 def branches
209 209 scm.branches
210 210 end
211 211
212 212 def tags
213 213 scm.tags
214 214 end
215 215
216 216 def default_branch
217 217 nil
218 218 end
219 219
220 220 def properties(path, identifier=nil)
221 221 scm.properties(path, identifier)
222 222 end
223 223
224 224 def cat(path, identifier=nil)
225 225 scm.cat(path, identifier)
226 226 end
227 227
228 228 def diff(path, rev, rev_to)
229 229 scm.diff(path, rev, rev_to)
230 230 end
231 231
232 232 def diff_format_revisions(cs, cs_to, sep=':')
233 233 text = ""
234 234 text << cs_to.format_identifier + sep if cs_to
235 235 text << cs.format_identifier if cs
236 236 text
237 237 end
238 238
239 239 # Returns a path relative to the url of the repository
240 240 def relative_path(path)
241 241 path
242 242 end
243 243
244 244 # Finds and returns a revision with a number or the beginning of a hash
245 245 def find_changeset_by_name(name)
246 246 return nil if name.blank?
247 247 s = name.to_s
248 248 if s.match(/^\d*$/)
249 249 changesets.where("revision = ?", s).first
250 250 else
251 251 changesets.where("revision LIKE ?", s + '%').first
252 252 end
253 253 end
254 254
255 255 def latest_changeset
256 256 @latest_changeset ||= changesets.first
257 257 end
258 258
259 259 # Returns the latest changesets for +path+
260 260 # Default behaviour is to search in cached changesets
261 261 def latest_changesets(path, rev, limit=10)
262 262 if path.blank?
263 263 changesets.
264 264 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
265 265 limit(limit).
266 266 preload(:user).
267 267 all
268 268 else
269 269 filechanges.
270 270 where("path = ?", path.with_leading_slash).
271 271 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
272 272 limit(limit).
273 273 preload(:changeset => :user).
274 274 collect(&:changeset)
275 275 end
276 276 end
277 277
278 278 def scan_changesets_for_issue_ids
279 279 self.changesets.each(&:scan_comment_for_issue_ids)
280 280 end
281 281
282 282 # Returns an array of committers usernames and associated user_id
283 283 def committers
284 284 @committers ||= Changeset.connection.select_rows(
285 285 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
286 286 end
287 287
288 288 # Maps committers username to a user ids
289 289 def committer_ids=(h)
290 290 if h.is_a?(Hash)
291 291 committers.each do |committer, user_id|
292 292 new_user_id = h[committer]
293 293 if new_user_id && (new_user_id.to_i != user_id.to_i)
294 294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
295 295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
296 296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
297 297 end
298 298 end
299 299 @committers = nil
300 300 @found_committer_users = nil
301 301 true
302 302 else
303 303 false
304 304 end
305 305 end
306 306
307 307 # Returns the Redmine User corresponding to the given +committer+
308 308 # It will return nil if the committer is not yet mapped and if no User
309 309 # with the same username or email was found
310 310 def find_committer_user(committer)
311 311 unless committer.blank?
312 312 @found_committer_users ||= {}
313 313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
314 314
315 315 user = nil
316 316 c = changesets.where(:committer => committer).includes(:user).first
317 317 if c && c.user
318 318 user = c.user
319 319 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
320 320 username, email = $1.strip, $3
321 321 u = User.find_by_login(username)
322 322 u ||= User.find_by_mail(email) unless email.blank?
323 323 user = u
324 324 end
325 325 @found_committer_users[committer] = user
326 326 user
327 327 end
328 328 end
329 329
330 330 def repo_log_encoding
331 331 encoding = log_encoding.to_s.strip
332 332 encoding.blank? ? 'UTF-8' : encoding
333 333 end
334 334
335 335 # Fetches new changesets for all repositories of active projects
336 336 # Can be called periodically by an external script
337 337 # eg. ruby script/runner "Repository.fetch_changesets"
338 338 def self.fetch_changesets
339 339 Project.active.has_module(:repository).all.each do |project|
340 340 project.repositories.each do |repository|
341 341 begin
342 342 repository.fetch_changesets
343 343 rescue Redmine::Scm::Adapters::CommandFailed => e
344 344 logger.error "scm: error during fetching changesets: #{e.message}"
345 345 end
346 346 end
347 347 end
348 348 end
349 349
350 350 # scan changeset comments to find related and fixed issues for all repositories
351 351 def self.scan_changesets_for_issue_ids
352 352 all.each(&:scan_changesets_for_issue_ids)
353 353 end
354 354
355 355 def self.scm_name
356 356 'Abstract'
357 357 end
358 358
359 359 def self.available_scm
360 360 subclasses.collect {|klass| [klass.scm_name, klass.name]}
361 361 end
362 362
363 363 def self.factory(klass_name, *args)
364 364 klass = "Repository::#{klass_name}".constantize
365 365 klass.new(*args)
366 366 rescue
367 367 nil
368 368 end
369 369
370 370 def self.scm_adapter_class
371 371 nil
372 372 end
373 373
374 374 def self.scm_command
375 375 ret = ""
376 376 begin
377 377 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
378 378 rescue Exception => e
379 379 logger.error "scm: error during get command: #{e.message}"
380 380 end
381 381 ret
382 382 end
383 383
384 384 def self.scm_version_string
385 385 ret = ""
386 386 begin
387 387 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
388 388 rescue Exception => e
389 389 logger.error "scm: error during get version string: #{e.message}"
390 390 end
391 391 ret
392 392 end
393 393
394 394 def self.scm_available
395 395 ret = false
396 396 begin
397 397 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
398 398 rescue Exception => e
399 399 logger.error "scm: error during get scm available: #{e.message}"
400 400 end
401 401 ret
402 402 end
403 403
404 404 def set_as_default?
405 405 new_record? && project && Repository.where(:project_id => project.id).empty?
406 406 end
407 407
408 408 # Returns a hash with statistics by author in the following form:
409 409 # {
410 410 # "John Smith" => { :commits => 45, :changes => 324 },
411 411 # "Bob" => { ... }
412 412 # }
413 413 #
414 414 # Notes:
415 415 # - this hash honnors the users mapping defined for the repository
416 416 def stats_by_author
417 417 commits = Changeset.where("repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
418 418
419 419 #TODO: restore ordering ; this line probably never worked
420 420 #commits.to_a.sort! {|x, y| x.last <=> y.last}
421 421
422 422 changes = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
423 423
424 424 user_ids = changesets.map(&:user_id).compact.uniq
425 425 authors_names = User.where(:id => user_ids).inject({}) do |memo, user|
426 426 memo[user.id] = user.to_s
427 427 memo
428 428 end
429 429
430 430 (commits + changes).inject({}) do |hash, element|
431 431 mapped_name = element.committer
432 432 if username = authors_names[element.user_id.to_i]
433 433 mapped_name = username
434 434 end
435 435 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
436 436 if element.is_a?(Changeset)
437 437 hash[mapped_name][:commits_count] += element.count.to_i
438 438 else
439 439 hash[mapped_name][:changes_count] += element.count.to_i
440 440 end
441 441 hash
442 442 end
443 443 end
444 444
445 # Returns a scope of changesets that come from the same commit as the given changeset
446 # in different repositories that point to the same backend
447 def same_commits_in_scope(scope, changeset)
448 scope = scope.joins(:repository).where(:repositories => {:url => url, :root_url => root_url, :type => type})
449 if changeset.scmid.present?
450 scope = scope.where(:scmid => changeset.scmid)
451 else
452 scope = scope.where(:revision => changeset.revision)
453 end
454 scope
455 end
456
445 457 protected
446 458
447 459 def check_default
448 460 if !is_default? && set_as_default?
449 461 self.is_default = true
450 462 end
451 463 if is_default? && is_default_changed?
452 464 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
453 465 end
454 466 end
455 467
456 468 def load_entries_changesets(entries)
457 469 if entries
458 470 entries.each do |entry|
459 471 if entry.lastrev && entry.lastrev.identifier
460 472 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
461 473 end
462 474 end
463 475 end
464 476 end
465 477
466 478 private
467 479
468 480 # Deletes repository data
469 481 def clear_changesets
470 482 cs = Changeset.table_name
471 483 ch = Change.table_name
472 484 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
473 485 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
474 486
475 487 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
476 488 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
477 489 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
478 490 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
479 491 clear_extra_info_of_changesets
480 492 end
481 493
482 494 def clear_extra_info_of_changesets
483 495 end
484 496 end
@@ -1,555 +1,566
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class ChangesetTest < ActiveSupport::TestCase
23 23 fixtures :projects, :repositories,
24 24 :issues, :issue_statuses, :issue_categories,
25 25 :changesets, :changes,
26 26 :enumerations,
27 27 :custom_fields, :custom_values,
28 28 :users, :members, :member_roles,
29 29 :trackers, :projects_trackers,
30 30 :enabled_modules, :roles
31 31
32 32 def test_ref_keywords_any
33 33 ActionMailer::Base.deliveries.clear
34 34 Setting.commit_ref_keywords = '*'
35 35 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => '5', 'done_ratio' => '90'}]
36 36
37 37 c = Changeset.new(:repository => Project.find(1).repository,
38 38 :committed_on => Time.now,
39 39 :comments => 'New commit (#2). Fixes #1',
40 40 :revision => '12345')
41 41 assert c.save
42 42 assert_equal [1, 2], c.issue_ids.sort
43 43 fixed = Issue.find(1)
44 44 assert fixed.closed?
45 45 assert_equal 90, fixed.done_ratio
46 46 assert_equal 1, ActionMailer::Base.deliveries.size
47 47 end
48 48
49 49 def test_ref_keywords
50 50 Setting.commit_ref_keywords = 'refs'
51 51 Setting.commit_update_keywords = ''
52 52 c = Changeset.new(:repository => Project.find(1).repository,
53 53 :committed_on => Time.now,
54 54 :comments => 'Ignores #2. Refs #1',
55 55 :revision => '12345')
56 56 assert c.save
57 57 assert_equal [1], c.issue_ids.sort
58 58 end
59 59
60 60 def test_ref_keywords_any_only
61 61 Setting.commit_ref_keywords = '*'
62 62 Setting.commit_update_keywords = ''
63 63 c = Changeset.new(:repository => Project.find(1).repository,
64 64 :committed_on => Time.now,
65 65 :comments => 'Ignores #2. Refs #1',
66 66 :revision => '12345')
67 67 assert c.save
68 68 assert_equal [1, 2], c.issue_ids.sort
69 69 end
70 70
71 71 def test_ref_keywords_any_with_timelog
72 72 Setting.commit_ref_keywords = '*'
73 73 Setting.commit_logtime_enabled = '1'
74 74
75 75 {
76 76 '2' => 2.0,
77 77 '2h' => 2.0,
78 78 '2hours' => 2.0,
79 79 '15m' => 0.25,
80 80 '15min' => 0.25,
81 81 '3h15' => 3.25,
82 82 '3h15m' => 3.25,
83 83 '3h15min' => 3.25,
84 84 '3:15' => 3.25,
85 85 '3.25' => 3.25,
86 86 '3.25h' => 3.25,
87 87 '3,25' => 3.25,
88 88 '3,25h' => 3.25,
89 89 }.each do |syntax, expected_hours|
90 90 c = Changeset.new(:repository => Project.find(1).repository,
91 91 :committed_on => 24.hours.ago,
92 92 :comments => "Worked on this issue #1 @#{syntax}",
93 93 :revision => '520',
94 94 :user => User.find(2))
95 95 assert_difference 'TimeEntry.count' do
96 96 c.scan_comment_for_issue_ids
97 97 end
98 98 assert_equal [1], c.issue_ids.sort
99 99
100 100 time = TimeEntry.order('id desc').first
101 101 assert_equal 1, time.issue_id
102 102 assert_equal 1, time.project_id
103 103 assert_equal 2, time.user_id
104 104 assert_equal expected_hours, time.hours,
105 105 "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}"
106 106 assert_equal Date.yesterday, time.spent_on
107 107 assert time.activity.is_default?
108 108 assert time.comments.include?('r520'),
109 109 "r520 was expected in time_entry comments: #{time.comments}"
110 110 end
111 111 end
112 112
113 113 def test_ref_keywords_closing_with_timelog
114 114 Setting.commit_ref_keywords = '*'
115 115 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes',
116 116 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s}]
117 117 Setting.commit_logtime_enabled = '1'
118 118
119 119 c = Changeset.new(:repository => Project.find(1).repository,
120 120 :committed_on => Time.now,
121 121 :comments => 'This is a comment. Fixes #1 @4.5, #2 @1',
122 122 :user => User.find(2))
123 123 assert_difference 'TimeEntry.count', 2 do
124 124 c.scan_comment_for_issue_ids
125 125 end
126 126
127 127 assert_equal [1, 2], c.issue_ids.sort
128 128 assert Issue.find(1).closed?
129 129 assert Issue.find(2).closed?
130 130
131 131 times = TimeEntry.order('id desc').limit(2)
132 132 assert_equal [1, 2], times.collect(&:issue_id).sort
133 133 end
134 134
135 135 def test_ref_keywords_any_line_start
136 136 Setting.commit_ref_keywords = '*'
137 137 c = Changeset.new(:repository => Project.find(1).repository,
138 138 :committed_on => Time.now,
139 139 :comments => '#1 is the reason of this commit',
140 140 :revision => '12345')
141 141 assert c.save
142 142 assert_equal [1], c.issue_ids.sort
143 143 end
144 144
145 145 def test_ref_keywords_allow_brackets_around_a_issue_number
146 146 Setting.commit_ref_keywords = '*'
147 147 c = Changeset.new(:repository => Project.find(1).repository,
148 148 :committed_on => Time.now,
149 149 :comments => '[#1] Worked on this issue',
150 150 :revision => '12345')
151 151 assert c.save
152 152 assert_equal [1], c.issue_ids.sort
153 153 end
154 154
155 155 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
156 156 Setting.commit_ref_keywords = '*'
157 157 c = Changeset.new(:repository => Project.find(1).repository,
158 158 :committed_on => Time.now,
159 159 :comments => '[#1 #2, #3] Worked on these',
160 160 :revision => '12345')
161 161 assert c.save
162 162 assert_equal [1,2,3], c.issue_ids.sort
163 163 end
164 164
165 165 def test_update_keywords_with_multiple_rules
166 166 with_settings :commit_update_keywords => [
167 167 {'keywords' => 'fixes, closes', 'status_id' => '5'},
168 168 {'keywords' => 'resolves', 'status_id' => '3'}
169 169 ] do
170 170
171 171 issue1 = Issue.generate!
172 172 issue2 = Issue.generate!
173 173 Changeset.generate!(:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}")
174 174 assert_equal 5, issue1.reload.status_id
175 175 assert_equal 3, issue2.reload.status_id
176 176 end
177 177 end
178 178
179 179 def test_update_keywords_with_multiple_rules_should_match_tracker
180 180 with_settings :commit_update_keywords => [
181 181 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
182 182 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => ''}
183 183 ] do
184 184
185 185 issue1 = Issue.generate!(:tracker_id => 2)
186 186 issue2 = Issue.generate!
187 187 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
188 188 assert_equal 5, issue1.reload.status_id
189 189 assert_equal 3, issue2.reload.status_id
190 190 end
191 191 end
192 192
193 193 def test_update_keywords_with_multiple_rules_and_no_match
194 194 with_settings :commit_update_keywords => [
195 195 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
196 196 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => '3'}
197 197 ] do
198 198
199 199 issue1 = Issue.generate!(:tracker_id => 2)
200 200 issue2 = Issue.generate!
201 201 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
202 202 assert_equal 5, issue1.reload.status_id
203 203 assert_equal 1, issue2.reload.status_id # no updates
204 204 end
205 205 end
206 206
207 207 def test_commit_referencing_a_subproject_issue
208 208 c = Changeset.new(:repository => Project.find(1).repository,
209 209 :committed_on => Time.now,
210 210 :comments => 'refs #5, a subproject issue',
211 211 :revision => '12345')
212 212 assert c.save
213 213 assert_equal [5], c.issue_ids.sort
214 214 assert c.issues.first.project != c.project
215 215 end
216 216
217 217 def test_commit_closing_a_subproject_issue
218 218 with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}],
219 219 :default_language => 'en' do
220 220 issue = Issue.find(5)
221 221 assert !issue.closed?
222 222 assert_difference 'Journal.count' do
223 223 c = Changeset.new(:repository => Project.find(1).repository,
224 224 :committed_on => Time.now,
225 225 :comments => 'closes #5, a subproject issue',
226 226 :revision => '12345')
227 227 assert c.save
228 228 end
229 229 assert issue.reload.closed?
230 230 journal = Journal.order('id DESC').first
231 231 assert_equal issue, journal.issue
232 232 assert_include "Applied in changeset ecookbook:r12345.", journal.notes
233 233 end
234 234 end
235 235
236 236 def test_commit_referencing_a_parent_project_issue
237 237 # repository of child project
238 238 r = Repository::Subversion.create!(
239 239 :project => Project.find(3),
240 240 :url => 'svn://localhost/test')
241 241 c = Changeset.new(:repository => r,
242 242 :committed_on => Time.now,
243 243 :comments => 'refs #2, an issue of a parent project',
244 244 :revision => '12345')
245 245 assert c.save
246 246 assert_equal [2], c.issue_ids.sort
247 247 assert c.issues.first.project != c.project
248 248 end
249 249
250 250 def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled
251 251 r = Repository::Subversion.create!(
252 252 :project => Project.find(3),
253 253 :url => 'svn://localhost/test')
254 254 with_settings :commit_cross_project_ref => '0' do
255 255 c = Changeset.new(:repository => r,
256 256 :committed_on => Time.now,
257 257 :comments => 'refs #4, an issue of a different project',
258 258 :revision => '12345')
259 259 assert c.save
260 260 assert_equal [], c.issue_ids
261 261 end
262 262 end
263 263
264 264 def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled
265 265 r = Repository::Subversion.create!(
266 266 :project => Project.find(3),
267 267 :url => 'svn://localhost/test')
268 268 with_settings :commit_cross_project_ref => '1' do
269 269 c = Changeset.new(:repository => r,
270 270 :committed_on => Time.now,
271 271 :comments => 'refs #4, an issue of a different project',
272 272 :revision => '12345')
273 273 assert c.save
274 274 assert_equal [4], c.issue_ids
275 275 end
276 276 end
277 277
278 278 def test_old_commits_should_not_update_issues_nor_log_time
279 279 Setting.commit_ref_keywords = '*'
280 280 Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}}
281 281 Setting.commit_logtime_enabled = '1'
282 282
283 283 repository = Project.find(1).repository
284 284 repository.created_on = Time.now
285 285 repository.save!
286 286
287 287 c = Changeset.new(:repository => repository,
288 288 :committed_on => 1.month.ago,
289 289 :comments => 'New commit (#2). Fixes #1 @1h',
290 290 :revision => '12345')
291 291 assert_no_difference 'TimeEntry.count' do
292 292 assert c.save
293 293 end
294 294 assert_equal [1, 2], c.issue_ids.sort
295 295 issue = Issue.find(1)
296 296 assert_equal 1, issue.status_id
297 297 assert_equal 0, issue.done_ratio
298 298 end
299 299
300 def test_2_repositories_with_same_backend_should_not_link_issue_multiple_times
301 Setting.commit_ref_keywords = '*'
302 r1 = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///svn1')
303 r2 = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn2', :url => 'file:///svn1')
304 now = Time.now
305 assert_difference 'Issue.find(1).changesets.count' do
306 c1 = Changeset.create!(:repository => r1, :committed_on => now, :comments => 'Fixes #1', :revision => '12345')
307 c1 = Changeset.create!(:repository => r2, :committed_on => now, :comments => 'Fixes #1', :revision => '12345')
308 end
309 end
310
300 311 def test_text_tag_revision
301 312 c = Changeset.new(:revision => '520')
302 313 assert_equal 'r520', c.text_tag
303 314 end
304 315
305 316 def test_text_tag_revision_with_same_project
306 317 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
307 318 assert_equal 'r520', c.text_tag(Project.find(1))
308 319 end
309 320
310 321 def test_text_tag_revision_with_different_project
311 322 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
312 323 assert_equal 'ecookbook:r520', c.text_tag(Project.find(2))
313 324 end
314 325
315 326 def test_text_tag_revision_with_repository_identifier
316 327 r = Repository::Subversion.create!(
317 328 :project_id => 1,
318 329 :url => 'svn://localhost/test',
319 330 :identifier => 'documents')
320 331 c = Changeset.new(:revision => '520', :repository => r)
321 332 assert_equal 'documents|r520', c.text_tag
322 333 assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2))
323 334 end
324 335
325 336 def test_text_tag_hash
326 337 c = Changeset.new(
327 338 :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518',
328 339 :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518')
329 340 assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag
330 341 end
331 342
332 343 def test_text_tag_hash_with_same_project
333 344 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
334 345 assert_equal 'commit:7234cb27', c.text_tag(Project.find(1))
335 346 end
336 347
337 348 def test_text_tag_hash_with_different_project
338 349 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
339 350 assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2))
340 351 end
341 352
342 353 def test_text_tag_hash_all_number
343 354 c = Changeset.new(:scmid => '0123456789', :revision => '0123456789')
344 355 assert_equal 'commit:0123456789', c.text_tag
345 356 end
346 357
347 358 def test_text_tag_hash_with_repository_identifier
348 359 r = Repository::Subversion.new(
349 360 :project_id => 1,
350 361 :url => 'svn://localhost/test',
351 362 :identifier => 'documents')
352 363 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => r)
353 364 assert_equal 'commit:documents|7234cb27', c.text_tag
354 365 assert_equal 'ecookbook:commit:documents|7234cb27', c.text_tag(Project.find(2))
355 366 end
356 367
357 368 def test_previous
358 369 changeset = Changeset.find_by_revision('3')
359 370 assert_equal Changeset.find_by_revision('2'), changeset.previous
360 371 end
361 372
362 373 def test_previous_nil
363 374 changeset = Changeset.find_by_revision('1')
364 375 assert_nil changeset.previous
365 376 end
366 377
367 378 def test_next
368 379 changeset = Changeset.find_by_revision('2')
369 380 assert_equal Changeset.find_by_revision('3'), changeset.next
370 381 end
371 382
372 383 def test_next_nil
373 384 changeset = Changeset.find_by_revision('10')
374 385 assert_nil changeset.next
375 386 end
376 387
377 388 def test_comments_should_be_converted_to_utf8
378 389 proj = Project.find(3)
379 390 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
380 391 str = "Texte encod\xe9 en ISO-8859-1."
381 392 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
382 393 r = Repository::Bazaar.create!(
383 394 :project => proj,
384 395 :url => '/tmp/test/bazaar',
385 396 :log_encoding => 'ISO-8859-1' )
386 397 assert r
387 398 c = Changeset.new(:repository => r,
388 399 :committed_on => Time.now,
389 400 :revision => '123',
390 401 :scmid => '12345',
391 402 :comments => str)
392 403 assert( c.save )
393 404 str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1."
394 405 str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding)
395 406 assert_equal str_utf8, c.comments
396 407 end
397 408
398 409 def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1
399 410 proj = Project.find(3)
400 411 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
401 412 str1 = "Texte encod\xe9 en ISO-8859-1."
402 413 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
403 414 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
404 415 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
405 416 r = Repository::Bazaar.create!(
406 417 :project => proj,
407 418 :url => '/tmp/test/bazaar',
408 419 :log_encoding => 'UTF-8' )
409 420 assert r
410 421 c = Changeset.new(:repository => r,
411 422 :committed_on => Time.now,
412 423 :revision => '123',
413 424 :scmid => '12345',
414 425 :comments => str1,
415 426 :committer => str2)
416 427 assert( c.save )
417 428 assert_equal "Texte encod? en ISO-8859-1.", c.comments
418 429 assert_equal "?a?b?c?d?e test", c.committer
419 430 end
420 431
421 432 def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis
422 433 proj = Project.find(3)
423 434 str = "test\xb5\xfetest\xb5\xfe"
424 435 if str.respond_to?(:force_encoding)
425 436 str.force_encoding('ASCII-8BIT')
426 437 end
427 438 r = Repository::Bazaar.create!(
428 439 :project => proj,
429 440 :url => '/tmp/test/bazaar',
430 441 :log_encoding => 'ISO-2022-JP' )
431 442 assert r
432 443 c = Changeset.new(:repository => r,
433 444 :committed_on => Time.now,
434 445 :revision => '123',
435 446 :scmid => '12345',
436 447 :comments => str)
437 448 assert( c.save )
438 449 assert_equal "test??test??", c.comments
439 450 end
440 451
441 452 def test_comments_should_be_converted_all_latin1_to_utf8
442 453 s1 = "\xC2\x80"
443 454 s2 = "\xc3\x82\xc2\x80"
444 455 s4 = s2.dup
445 456 if s1.respond_to?(:force_encoding)
446 457 s3 = s1.dup
447 458 s1.force_encoding('ASCII-8BIT')
448 459 s2.force_encoding('ASCII-8BIT')
449 460 s3.force_encoding('ISO-8859-1')
450 461 s4.force_encoding('UTF-8')
451 462 assert_equal s3.encode('UTF-8'), s4
452 463 end
453 464 proj = Project.find(3)
454 465 r = Repository::Bazaar.create!(
455 466 :project => proj,
456 467 :url => '/tmp/test/bazaar',
457 468 :log_encoding => 'ISO-8859-1' )
458 469 assert r
459 470 c = Changeset.new(:repository => r,
460 471 :committed_on => Time.now,
461 472 :revision => '123',
462 473 :scmid => '12345',
463 474 :comments => s1)
464 475 assert( c.save )
465 476 assert_equal s4, c.comments
466 477 end
467 478
468 479 def test_invalid_utf8_sequences_in_paths_should_be_replaced
469 480 proj = Project.find(3)
470 481 str1 = "Texte encod\xe9 en ISO-8859-1"
471 482 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
472 483 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
473 484 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
474 485 r = Repository::Bazaar.create!(
475 486 :project => proj,
476 487 :url => '/tmp/test/bazaar',
477 488 :log_encoding => 'UTF-8' )
478 489 assert r
479 490 cs = Changeset.new(
480 491 :repository => r,
481 492 :committed_on => Time.now,
482 493 :revision => '123',
483 494 :scmid => '12345',
484 495 :comments => "test")
485 496 assert(cs.save)
486 497 ch = Change.new(
487 498 :changeset => cs,
488 499 :action => "A",
489 500 :path => str1,
490 501 :from_path => str2,
491 502 :from_revision => "345")
492 503 assert(ch.save)
493 504 assert_equal "Texte encod? en ISO-8859-1", ch.path
494 505 assert_equal "?a?b?c?d?e test", ch.from_path
495 506 end
496 507
497 508 def test_comments_nil
498 509 proj = Project.find(3)
499 510 r = Repository::Bazaar.create!(
500 511 :project => proj,
501 512 :url => '/tmp/test/bazaar',
502 513 :log_encoding => 'ISO-8859-1' )
503 514 assert r
504 515 c = Changeset.new(:repository => r,
505 516 :committed_on => Time.now,
506 517 :revision => '123',
507 518 :scmid => '12345',
508 519 :comments => nil,
509 520 :committer => nil)
510 521 assert( c.save )
511 522 assert_equal "", c.comments
512 523 assert_equal nil, c.committer
513 524 if c.comments.respond_to?(:force_encoding)
514 525 assert_equal "UTF-8", c.comments.encoding.to_s
515 526 end
516 527 end
517 528
518 529 def test_comments_empty
519 530 proj = Project.find(3)
520 531 r = Repository::Bazaar.create!(
521 532 :project => proj,
522 533 :url => '/tmp/test/bazaar',
523 534 :log_encoding => 'ISO-8859-1' )
524 535 assert r
525 536 c = Changeset.new(:repository => r,
526 537 :committed_on => Time.now,
527 538 :revision => '123',
528 539 :scmid => '12345',
529 540 :comments => "",
530 541 :committer => "")
531 542 assert( c.save )
532 543 assert_equal "", c.comments
533 544 assert_equal "", c.committer
534 545 if c.comments.respond_to?(:force_encoding)
535 546 assert_equal "UTF-8", c.comments.encoding.to_s
536 547 assert_equal "UTF-8", c.committer.encoding.to_s
537 548 end
538 549 end
539 550
540 551 def test_comments_should_accept_more_than_64k
541 552 c = Changeset.new(:repository => Repository.first,
542 553 :committed_on => Time.now,
543 554 :revision => '123',
544 555 :scmid => '12345',
545 556 :comments => "a" * 500.kilobyte)
546 557 assert c.save
547 558 c.reload
548 559 assert_equal 500.kilobyte, c.comments.size
549 560 end
550 561
551 562 def test_identifier
552 563 c = Changeset.find_by_revision('1')
553 564 assert_equal c.revision, c.identifier
554 565 end
555 566 end
General Comments 0
You need to be logged in to leave comments. Login now