##// END OF EJS Templates
scm: ignore log encoding setting in Subversion and Mercurial (#7597)....
Toshi MARUYAMA -
r4842:06f078a8137a
parent child
Show More
@@ -1,275 +1,272
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.first || User.current, :view_changesets) } }
45 :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
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 comments=(comment)
61 write_attribute(:comments, Changeset.normalize_comments(comment))
62 end
63
64 def committed_on=(date)
60 def committed_on=(date)
65 self.commit_date = date
61 self.commit_date = date
66 super
62 super
67 end
63 end
68
64
69 # Returns the readable identifier
65 # Returns the readable identifier
70 def format_identifier
66 def format_identifier
71 if repository.class.respond_to? :format_changeset_identifier
67 if repository.class.respond_to? :format_changeset_identifier
72 repository.class.format_changeset_identifier self
68 repository.class.format_changeset_identifier self
73 else
69 else
74 identifier
70 identifier
75 end
71 end
76 end
72 end
77
73
78 def committer=(arg)
79 write_attribute(:committer, self.class.to_utf8(arg.to_s))
80 end
81
82 def project
74 def project
83 repository.project
75 repository.project
84 end
76 end
85
77
86 def author
78 def author
87 user || committer.to_s.split('<').first
79 user || committer.to_s.split('<').first
88 end
80 end
89
81
90 def before_create
82 def before_create
91 self.user = repository.find_committer_user(committer)
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)
85 self.user = repository.find_committer_user(self.committer)
92 end
86 end
93
87
94 def after_create
88 def after_create
95 scan_comment_for_issue_ids
89 scan_comment_for_issue_ids
96 end
90 end
97
91
98 TIMELOG_RE = /
92 TIMELOG_RE = /
99 (
93 (
100 ((\d+)(h|hours?))((\d+)(m|min)?)?
94 ((\d+)(h|hours?))((\d+)(m|min)?)?
101 |
95 |
102 ((\d+)(h|hours?|m|min))
96 ((\d+)(h|hours?|m|min))
103 |
97 |
104 (\d+):(\d+)
98 (\d+):(\d+)
105 |
99 |
106 (\d+([\.,]\d+)?)h?
100 (\d+([\.,]\d+)?)h?
107 )
101 )
108 /x
102 /x
109
103
110 def scan_comment_for_issue_ids
104 def scan_comment_for_issue_ids
111 return if comments.blank?
105 return if comments.blank?
112 # keywords used to reference issues
106 # keywords used to reference issues
113 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
107 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
114 ref_keywords_any = ref_keywords.delete('*')
108 ref_keywords_any = ref_keywords.delete('*')
115 # keywords used to fix issues
109 # keywords used to fix issues
116 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
110 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
117
111
118 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
112 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
119
113
120 referenced_issues = []
114 referenced_issues = []
121
115
122 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
116 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
123 action, refs = match[2], match[3]
117 action, refs = match[2], match[3]
124 next unless action.present? || ref_keywords_any
118 next unless action.present? || ref_keywords_any
125
119
126 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
120 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
127 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
121 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
128 if issue
122 if issue
129 referenced_issues << issue
123 referenced_issues << issue
130 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
124 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
131 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
125 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
132 end
126 end
133 end
127 end
134 end
128 end
135
129
136 referenced_issues.uniq!
130 referenced_issues.uniq!
137 self.issues = referenced_issues unless referenced_issues.empty?
131 self.issues = referenced_issues unless referenced_issues.empty?
138 end
132 end
139
133
140 def short_comments
134 def short_comments
141 @short_comments || split_comments.first
135 @short_comments || split_comments.first
142 end
136 end
143
137
144 def long_comments
138 def long_comments
145 @long_comments || split_comments.last
139 @long_comments || split_comments.last
146 end
140 end
147
141
148 def text_tag
142 def text_tag
149 if scmid?
143 if scmid?
150 "commit:#{scmid}"
144 "commit:#{scmid}"
151 else
145 else
152 "r#{revision}"
146 "r#{revision}"
153 end
147 end
154 end
148 end
155
149
156 # Returns the previous changeset
150 # Returns the previous changeset
157 def previous
151 def previous
158 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
152 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
159 end
153 end
160
154
161 # Returns the next changeset
155 # Returns the next changeset
162 def next
156 def next
163 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
157 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
164 end
158 end
165
159
166 # Strips and reencodes a commit log before insertion into the database
167 def self.normalize_comments(str)
168 to_utf8(str.to_s.strip)
169 end
170
171 # Creates a new Change from it's common parameters
160 # Creates a new Change from it's common parameters
172 def create_change(change)
161 def create_change(change)
173 Change.create(:changeset => self,
162 Change.create(:changeset => self,
174 :action => change[:action],
163 :action => change[:action],
175 :path => change[:path],
164 :path => change[:path],
176 :from_path => change[:from_path],
165 :from_path => change[:from_path],
177 :from_revision => change[:from_revision])
166 :from_revision => change[:from_revision])
178 end
167 end
179
168
180 private
169 private
181
170
182 # Finds an issue that can be referenced by the commit message
171 # Finds an issue that can be referenced by the commit message
183 # i.e. an issue that belong to the repository project, a subproject or a parent project
172 # i.e. an issue that belong to the repository project, a subproject or a parent project
184 def find_referenced_issue_by_id(id)
173 def find_referenced_issue_by_id(id)
185 return nil if id.blank?
174 return nil if id.blank?
186 issue = Issue.find_by_id(id.to_i, :include => :project)
175 issue = Issue.find_by_id(id.to_i, :include => :project)
187 if issue
176 if issue
188 unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
177 unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
189 issue = nil
178 issue = nil
190 end
179 end
191 end
180 end
192 issue
181 issue
193 end
182 end
194
183
195 def fix_issue(issue)
184 def fix_issue(issue)
196 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
185 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
197 if status.nil?
186 if status.nil?
198 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
187 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
199 return issue
188 return issue
200 end
189 end
201
190
202 # the issue may have been updated by the closure of another one (eg. duplicate)
191 # the issue may have been updated by the closure of another one (eg. duplicate)
203 issue.reload
192 issue.reload
204 # don't change the status is the issue is closed
193 # don't change the status is the issue is closed
205 return if issue.status && issue.status.is_closed?
194 return if issue.status && issue.status.is_closed?
206
195
207 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
196 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
208 issue.status = status
197 issue.status = status
209 unless Setting.commit_fix_done_ratio.blank?
198 unless Setting.commit_fix_done_ratio.blank?
210 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
199 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
211 end
200 end
212 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
201 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
213 { :changeset => self, :issue => issue })
202 { :changeset => self, :issue => issue })
214 unless issue.save
203 unless issue.save
215 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
204 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
216 end
205 end
217 issue
206 issue
218 end
207 end
219
208
220 def log_time(issue, hours)
209 def log_time(issue, hours)
221 time_entry = TimeEntry.new(
210 time_entry = TimeEntry.new(
222 :user => user,
211 :user => user,
223 :hours => hours,
212 :hours => hours,
224 :issue => issue,
213 :issue => issue,
225 :spent_on => commit_date,
214 :spent_on => commit_date,
226 :comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
215 :comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
227 )
216 )
228 time_entry.activity = log_time_activity unless log_time_activity.nil?
217 time_entry.activity = log_time_activity unless log_time_activity.nil?
229
218
230 unless time_entry.save
219 unless time_entry.save
231 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
220 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
232 end
221 end
233 time_entry
222 time_entry
234 end
223 end
235
224
236 def log_time_activity
225 def log_time_activity
237 if Setting.commit_logtime_activity_id.to_i > 0
226 if Setting.commit_logtime_activity_id.to_i > 0
238 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
227 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
239 end
228 end
240 end
229 end
241
230
242 def split_comments
231 def split_comments
243 comments =~ /\A(.+?)\r?\n(.*)$/m
232 comments =~ /\A(.+?)\r?\n(.*)$/m
244 @short_comments = $1 || comments
233 @short_comments = $1 || comments
245 @long_comments = $2.to_s.strip
234 @long_comments = $2.to_s.strip
246 return @short_comments, @long_comments
235 return @short_comments, @long_comments
247 end
236 end
248
237
249 def self.to_utf8(str)
238 public
239
240 # Strips and reencodes a commit log before insertion into the database
241 def self.normalize_comments(str, encoding)
242 Changeset.to_utf8(str.to_s.strip, encoding)
243 end
244
245 private
246
247 def self.to_utf8(str, encoding)
250 return str if str.blank?
248 return str if str.blank?
251 encoding = Setting.commit_logs_encoding.to_s.strip
252 unless encoding.blank? || encoding == 'UTF-8'
249 unless encoding.blank? || encoding == 'UTF-8'
253 begin
250 begin
254 str = Iconv.conv('UTF-8', encoding, str)
251 str = Iconv.conv('UTF-8', encoding, str)
255 rescue Iconv::Failure
252 rescue Iconv::Failure
256 # do nothing here
253 # do nothing here
257 end
254 end
258 end
255 end
259 if str.respond_to?(:force_encoding)
256 if str.respond_to?(:force_encoding)
260 str.force_encoding('UTF-8')
257 str.force_encoding('UTF-8')
261 if ! str.valid_encoding?
258 if ! str.valid_encoding?
262 str = str.encode("US-ASCII", :invalid => :replace,
259 str = str.encode("US-ASCII", :invalid => :replace,
263 :undef => :replace, :replace => '?').encode("UTF-8")
260 :undef => :replace, :replace => '?').encode("UTF-8")
264 end
261 end
265 else
262 else
266 # removes invalid UTF8 sequences
263 # removes invalid UTF8 sequences
267 begin
264 begin
268 str = Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
265 str = Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
269 rescue Iconv::InvalidEncoding
266 rescue Iconv::InvalidEncoding
270 # "UTF-8//IGNORE" is not supported on some OS
267 # "UTF-8//IGNORE" is not supported on some OS
271 end
268 end
272 end
269 end
273 str
270 str
274 end
271 end
275 end
272 end
@@ -1,277 +1,282
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 class Repository < ActiveRecord::Base
18 class Repository < ActiveRecord::Base
19 include Redmine::Ciphering
19 include Redmine::Ciphering
20
20
21 belongs_to :project
21 belongs_to :project
22 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
22 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
23 has_many :changes, :through => :changesets
23 has_many :changes, :through => :changesets
24
24
25 # Raw SQL to delete changesets and changes in the database
25 # Raw SQL to delete changesets and changes in the database
26 # has_many :changesets, :dependent => :destroy is too slow for big repositories
26 # has_many :changesets, :dependent => :destroy is too slow for big repositories
27 before_destroy :clear_changesets
27 before_destroy :clear_changesets
28
28
29 validates_length_of :password, :maximum => 255, :allow_nil => true
29 validates_length_of :password, :maximum => 255, :allow_nil => true
30 # Checks if the SCM is enabled when creating a repository
30 # Checks if the SCM is enabled when creating a repository
31 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
31 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
32
32
33 # Removes leading and trailing whitespace
33 # Removes leading and trailing whitespace
34 def url=(arg)
34 def url=(arg)
35 write_attribute(:url, arg ? arg.to_s.strip : nil)
35 write_attribute(:url, arg ? arg.to_s.strip : nil)
36 end
36 end
37
37
38 # Removes leading and trailing whitespace
38 # Removes leading and trailing whitespace
39 def root_url=(arg)
39 def root_url=(arg)
40 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
40 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
41 end
41 end
42
42
43 def password
43 def password
44 read_ciphered_attribute(:password)
44 read_ciphered_attribute(:password)
45 end
45 end
46
46
47 def password=(arg)
47 def password=(arg)
48 write_ciphered_attribute(:password, arg)
48 write_ciphered_attribute(:password, arg)
49 end
49 end
50
50
51 def scm_adapter
51 def scm_adapter
52 self.class.scm_adapter_class
52 self.class.scm_adapter_class
53 end
53 end
54
54
55 def scm
55 def scm
56 @scm ||= self.scm_adapter.new(url, root_url,
56 @scm ||= self.scm_adapter.new(url, root_url,
57 login, password, path_encoding)
57 login, password, path_encoding)
58 update_attribute(:root_url, @scm.root_url) if root_url.blank?
58 update_attribute(:root_url, @scm.root_url) if root_url.blank?
59 @scm
59 @scm
60 end
60 end
61
61
62 def scm_name
62 def scm_name
63 self.class.scm_name
63 self.class.scm_name
64 end
64 end
65
65
66 def supports_cat?
66 def supports_cat?
67 scm.supports_cat?
67 scm.supports_cat?
68 end
68 end
69
69
70 def supports_annotate?
70 def supports_annotate?
71 scm.supports_annotate?
71 scm.supports_annotate?
72 end
72 end
73
73
74 def entry(path=nil, identifier=nil)
74 def entry(path=nil, identifier=nil)
75 scm.entry(path, identifier)
75 scm.entry(path, identifier)
76 end
76 end
77
77
78 def entries(path=nil, identifier=nil)
78 def entries(path=nil, identifier=nil)
79 scm.entries(path, identifier)
79 scm.entries(path, identifier)
80 end
80 end
81
81
82 def branches
82 def branches
83 scm.branches
83 scm.branches
84 end
84 end
85
85
86 def tags
86 def tags
87 scm.tags
87 scm.tags
88 end
88 end
89
89
90 def default_branch
90 def default_branch
91 scm.default_branch
91 scm.default_branch
92 end
92 end
93
93
94 def properties(path, identifier=nil)
94 def properties(path, identifier=nil)
95 scm.properties(path, identifier)
95 scm.properties(path, identifier)
96 end
96 end
97
97
98 def cat(path, identifier=nil)
98 def cat(path, identifier=nil)
99 scm.cat(path, identifier)
99 scm.cat(path, identifier)
100 end
100 end
101
101
102 def diff(path, rev, rev_to)
102 def diff(path, rev, rev_to)
103 scm.diff(path, rev, rev_to)
103 scm.diff(path, rev, rev_to)
104 end
104 end
105
105
106 def diff_format_revisions(cs, cs_to, sep=':')
106 def diff_format_revisions(cs, cs_to, sep=':')
107 text = ""
107 text = ""
108 text << cs_to.format_identifier + sep if cs_to
108 text << cs_to.format_identifier + sep if cs_to
109 text << cs.format_identifier if cs
109 text << cs.format_identifier if cs
110 text
110 text
111 end
111 end
112
112
113 # Returns a path relative to the url of the repository
113 # Returns a path relative to the url of the repository
114 def relative_path(path)
114 def relative_path(path)
115 path
115 path
116 end
116 end
117
117
118 # Finds and returns a revision with a number or the beginning of a hash
118 # Finds and returns a revision with a number or the beginning of a hash
119 def find_changeset_by_name(name)
119 def find_changeset_by_name(name)
120 return nil if name.blank?
120 return nil if name.blank?
121 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
121 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
122 end
122 end
123
123
124 def latest_changeset
124 def latest_changeset
125 @latest_changeset ||= changesets.find(:first)
125 @latest_changeset ||= changesets.find(:first)
126 end
126 end
127
127
128 # Returns the latest changesets for +path+
128 # Returns the latest changesets for +path+
129 # Default behaviour is to search in cached changesets
129 # Default behaviour is to search in cached changesets
130 def latest_changesets(path, rev, limit=10)
130 def latest_changesets(path, rev, limit=10)
131 if path.blank?
131 if path.blank?
132 changesets.find(:all, :include => :user,
132 changesets.find(:all, :include => :user,
133 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
133 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
134 :limit => limit)
134 :limit => limit)
135 else
135 else
136 changes.find(:all, :include => {:changeset => :user},
136 changes.find(:all, :include => {:changeset => :user},
137 :conditions => ["path = ?", path.with_leading_slash],
137 :conditions => ["path = ?", path.with_leading_slash],
138 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
138 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
139 :limit => limit).collect(&:changeset)
139 :limit => limit).collect(&:changeset)
140 end
140 end
141 end
141 end
142
142
143 def scan_changesets_for_issue_ids
143 def scan_changesets_for_issue_ids
144 self.changesets.each(&:scan_comment_for_issue_ids)
144 self.changesets.each(&:scan_comment_for_issue_ids)
145 end
145 end
146
146
147 # Returns an array of committers usernames and associated user_id
147 # Returns an array of committers usernames and associated user_id
148 def committers
148 def committers
149 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
149 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
150 end
150 end
151
151
152 # Maps committers username to a user ids
152 # Maps committers username to a user ids
153 def committer_ids=(h)
153 def committer_ids=(h)
154 if h.is_a?(Hash)
154 if h.is_a?(Hash)
155 committers.each do |committer, user_id|
155 committers.each do |committer, user_id|
156 new_user_id = h[committer]
156 new_user_id = h[committer]
157 if new_user_id && (new_user_id.to_i != user_id.to_i)
157 if new_user_id && (new_user_id.to_i != user_id.to_i)
158 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
158 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
159 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
159 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
160 end
160 end
161 end
161 end
162 @committers = nil
162 @committers = nil
163 @found_committer_users = nil
163 @found_committer_users = nil
164 true
164 true
165 else
165 else
166 false
166 false
167 end
167 end
168 end
168 end
169
169
170 # Returns the Redmine User corresponding to the given +committer+
170 # Returns the Redmine User corresponding to the given +committer+
171 # It will return nil if the committer is not yet mapped and if no User
171 # It will return nil if the committer is not yet mapped and if no User
172 # with the same username or email was found
172 # with the same username or email was found
173 def find_committer_user(committer)
173 def find_committer_user(committer)
174 unless committer.blank?
174 unless committer.blank?
175 @found_committer_users ||= {}
175 @found_committer_users ||= {}
176 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
176 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
177
177
178 user = nil
178 user = nil
179 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
179 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
180 if c && c.user
180 if c && c.user
181 user = c.user
181 user = c.user
182 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
182 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
183 username, email = $1.strip, $3
183 username, email = $1.strip, $3
184 u = User.find_by_login(username)
184 u = User.find_by_login(username)
185 u ||= User.find_by_mail(email) unless email.blank?
185 u ||= User.find_by_mail(email) unless email.blank?
186 user = u
186 user = u
187 end
187 end
188 @found_committer_users[committer] = user
188 @found_committer_users[committer] = user
189 user
189 user
190 end
190 end
191 end
191 end
192
192
193 def repo_log_encoding
194 encoding = Setting.commit_logs_encoding.to_s.strip
195 encoding.blank? ? 'UTF-8' : encoding
196 end
197
193 # Fetches new changesets for all repositories of active projects
198 # Fetches new changesets for all repositories of active projects
194 # Can be called periodically by an external script
199 # Can be called periodically by an external script
195 # eg. ruby script/runner "Repository.fetch_changesets"
200 # eg. ruby script/runner "Repository.fetch_changesets"
196 def self.fetch_changesets
201 def self.fetch_changesets
197 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
202 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
198 if project.repository
203 if project.repository
199 begin
204 begin
200 project.repository.fetch_changesets
205 project.repository.fetch_changesets
201 rescue Redmine::Scm::Adapters::CommandFailed => e
206 rescue Redmine::Scm::Adapters::CommandFailed => e
202 logger.error "scm: error during fetching changesets: #{e.message}"
207 logger.error "scm: error during fetching changesets: #{e.message}"
203 end
208 end
204 end
209 end
205 end
210 end
206 end
211 end
207
212
208 # scan changeset comments to find related and fixed issues for all repositories
213 # scan changeset comments to find related and fixed issues for all repositories
209 def self.scan_changesets_for_issue_ids
214 def self.scan_changesets_for_issue_ids
210 find(:all).each(&:scan_changesets_for_issue_ids)
215 find(:all).each(&:scan_changesets_for_issue_ids)
211 end
216 end
212
217
213 def self.scm_name
218 def self.scm_name
214 'Abstract'
219 'Abstract'
215 end
220 end
216
221
217 def self.available_scm
222 def self.available_scm
218 subclasses.collect {|klass| [klass.scm_name, klass.name]}
223 subclasses.collect {|klass| [klass.scm_name, klass.name]}
219 end
224 end
220
225
221 def self.factory(klass_name, *args)
226 def self.factory(klass_name, *args)
222 klass = "Repository::#{klass_name}".constantize
227 klass = "Repository::#{klass_name}".constantize
223 klass.new(*args)
228 klass.new(*args)
224 rescue
229 rescue
225 nil
230 nil
226 end
231 end
227
232
228 def self.scm_adapter_class
233 def self.scm_adapter_class
229 nil
234 nil
230 end
235 end
231
236
232 def self.scm_command
237 def self.scm_command
233 ret = ""
238 ret = ""
234 begin
239 begin
235 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
240 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
236 rescue Redmine::Scm::Adapters::CommandFailed => e
241 rescue Redmine::Scm::Adapters::CommandFailed => e
237 logger.error "scm: error during get command: #{e.message}"
242 logger.error "scm: error during get command: #{e.message}"
238 end
243 end
239 ret
244 ret
240 end
245 end
241
246
242 def self.scm_version_string
247 def self.scm_version_string
243 ret = ""
248 ret = ""
244 begin
249 begin
245 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
250 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
246 rescue Redmine::Scm::Adapters::CommandFailed => e
251 rescue Redmine::Scm::Adapters::CommandFailed => e
247 logger.error "scm: error during get version string: #{e.message}"
252 logger.error "scm: error during get version string: #{e.message}"
248 end
253 end
249 ret
254 ret
250 end
255 end
251
256
252 def self.scm_available
257 def self.scm_available
253 ret = false
258 ret = false
254 begin
259 begin
255 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
260 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
256 rescue Redmine::Scm::Adapters::CommandFailed => e
261 rescue Redmine::Scm::Adapters::CommandFailed => e
257 logger.error "scm: error during get scm available: #{e.message}"
262 logger.error "scm: error during get scm available: #{e.message}"
258 end
263 end
259 ret
264 ret
260 end
265 end
261
266
262 private
267 private
263
268
264 def before_save
269 def before_save
265 # Strips url and root_url
270 # Strips url and root_url
266 url.strip!
271 url.strip!
267 root_url.strip!
272 root_url.strip!
268 true
273 true
269 end
274 end
270
275
271 def clear_changesets
276 def clear_changesets
272 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
277 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
273 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
278 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
274 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
279 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
275 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
280 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
276 end
281 end
277 end
282 end
@@ -1,170 +1,171
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'redmine/scm/adapters/cvs_adapter'
18 require 'redmine/scm/adapters/cvs_adapter'
19 require 'digest/sha1'
19 require 'digest/sha1'
20
20
21 class Repository::Cvs < Repository
21 class Repository::Cvs < Repository
22 validates_presence_of :url, :root_url
22 validates_presence_of :url, :root_url
23
23
24 def self.scm_adapter_class
24 def self.scm_adapter_class
25 Redmine::Scm::Adapters::CvsAdapter
25 Redmine::Scm::Adapters::CvsAdapter
26 end
26 end
27
27
28 def self.scm_name
28 def self.scm_name
29 'CVS'
29 'CVS'
30 end
30 end
31
31
32 def entry(path=nil, identifier=nil)
32 def entry(path=nil, identifier=nil)
33 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
33 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
34 scm.entry(path, rev.nil? ? nil : rev.committed_on)
34 scm.entry(path, rev.nil? ? nil : rev.committed_on)
35 end
35 end
36
36
37 def entries(path=nil, identifier=nil)
37 def entries(path=nil, identifier=nil)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
40 if entries
40 if entries
41 entries.each() do |entry|
41 entries.each() do |entry|
42 unless entry.lastrev.nil? || entry.lastrev.identifier
42 unless entry.lastrev.nil? || entry.lastrev.identifier
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
44 if change
44 if change
45 entry.lastrev.identifier=change.changeset.revision
45 entry.lastrev.identifier=change.changeset.revision
46 entry.lastrev.author=change.changeset.committer
46 entry.lastrev.author=change.changeset.committer
47 entry.lastrev.revision=change.revision
47 entry.lastrev.revision=change.revision
48 entry.lastrev.branch=change.branch
48 entry.lastrev.branch=change.branch
49 end
49 end
50 end
50 end
51 end
51 end
52 end
52 end
53 entries
53 entries
54 end
54 end
55
55
56 def cat(path, identifier=nil)
56 def cat(path, identifier=nil)
57 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
57 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
58 scm.cat(path, rev.nil? ? nil : rev.committed_on)
58 scm.cat(path, rev.nil? ? nil : rev.committed_on)
59 end
59 end
60
60
61 def diff(path, rev, rev_to)
61 def diff(path, rev, rev_to)
62 #convert rev to revision. CVS can't handle changesets here
62 #convert rev to revision. CVS can't handle changesets here
63 diff=[]
63 diff=[]
64 changeset_from=changesets.find_by_revision(rev)
64 changeset_from=changesets.find_by_revision(rev)
65 if rev_to.to_i > 0
65 if rev_to.to_i > 0
66 changeset_to=changesets.find_by_revision(rev_to)
66 changeset_to=changesets.find_by_revision(rev_to)
67 end
67 end
68 changeset_from.changes.each() do |change_from|
68 changeset_from.changes.each() do |change_from|
69
69
70 revision_from=nil
70 revision_from=nil
71 revision_to=nil
71 revision_to=nil
72
72
73 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
73 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
74
74
75 if revision_from
75 if revision_from
76 if changeset_to
76 if changeset_to
77 changeset_to.changes.each() do |change_to|
77 changeset_to.changes.each() do |change_to|
78 revision_to=change_to.revision if change_to.path==change_from.path
78 revision_to=change_to.revision if change_to.path==change_from.path
79 end
79 end
80 end
80 end
81 unless revision_to
81 unless revision_to
82 revision_to=scm.get_previous_revision(revision_from)
82 revision_to=scm.get_previous_revision(revision_from)
83 end
83 end
84 file_diff = scm.diff(change_from.path, revision_from, revision_to)
84 file_diff = scm.diff(change_from.path, revision_from, revision_to)
85 diff = diff + file_diff unless file_diff.nil?
85 diff = diff + file_diff unless file_diff.nil?
86 end
86 end
87 end
87 end
88 return diff
88 return diff
89 end
89 end
90
90
91 def fetch_changesets
91 def fetch_changesets
92 # some nifty bits to introduce a commit-id with cvs
92 # some nifty bits to introduce a commit-id with cvs
93 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
93 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
94 # we now take a guess using the author, the commitlog and the commit-date.
94 # we now take a guess using the author, the commitlog and the commit-date.
95
95
96 # last one is the next step to take. the commit-date is not equal for all
96 # last one is the next step to take. the commit-date is not equal for all
97 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
97 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
98 # we use a small delta here, to merge all changes belonging to _one_ changeset
98 # we use a small delta here, to merge all changes belonging to _one_ changeset
99 time_delta=10.seconds
99 time_delta=10.seconds
100
100
101 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
101 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
102 transaction do
102 transaction do
103 tmp_rev_num = 1
103 tmp_rev_num = 1
104 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
104 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
105 # only add the change to the database, if it doen't exists. the cvs log
105 # only add the change to the database, if it doen't exists. the cvs log
106 # is not exclusive at all.
106 # is not exclusive at all.
107 tmp_time = revision.time.clone
107 tmp_time = revision.time.clone
108 unless changes.find_by_path_and_revision(
108 unless changes.find_by_path_and_revision(
109 scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
109 scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
110 cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
110 cs = changesets.find(:first, :conditions=>{
111 cs = changesets.find(:first, :conditions=>{
111 :committed_on=>tmp_time - time_delta .. tmp_time + time_delta,
112 :committed_on=>tmp_time - time_delta .. tmp_time + time_delta,
112 :committer=>revision.author,
113 :committer=>revision.author,
113 :comments=>Changeset.normalize_comments(revision.message)
114 :comments=>cmt
114 })
115 })
115
116
116 # create a new changeset....
117 # create a new changeset....
117 unless cs
118 unless cs
118 # we use a temporaray revision number here (just for inserting)
119 # we use a temporaray revision number here (just for inserting)
119 # later on, we calculate a continous positive number
120 # later on, we calculate a continous positive number
120 tmp_time2 = tmp_time.clone.gmtime
121 tmp_time2 = tmp_time.clone.gmtime
121 branch = revision.paths[0][:branch]
122 branch = revision.paths[0][:branch]
122 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
123 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
123 cs = Changeset.create(:repository => self,
124 cs = Changeset.create(:repository => self,
124 :revision => "tmp#{tmp_rev_num}",
125 :revision => "tmp#{tmp_rev_num}",
125 :scmid => scmid,
126 :scmid => scmid,
126 :committer => revision.author,
127 :committer => revision.author,
127 :committed_on => tmp_time,
128 :committed_on => tmp_time,
128 :comments => revision.message)
129 :comments => revision.message)
129 tmp_rev_num += 1
130 tmp_rev_num += 1
130 end
131 end
131
132
132 #convert CVS-File-States to internal Action-abbrevations
133 #convert CVS-File-States to internal Action-abbrevations
133 #default action is (M)odified
134 #default action is (M)odified
134 action="M"
135 action="M"
135 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
136 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
136 action="A" #add-action always at first revision (= 1.1)
137 action="A" #add-action always at first revision (= 1.1)
137 elsif revision.paths[0][:action]=="dead"
138 elsif revision.paths[0][:action]=="dead"
138 action="D" #dead-state is similar to Delete
139 action="D" #dead-state is similar to Delete
139 end
140 end
140
141
141 Change.create(:changeset => cs,
142 Change.create(:changeset => cs,
142 :action => action,
143 :action => action,
143 :path => scm.with_leading_slash(revision.paths[0][:path]),
144 :path => scm.with_leading_slash(revision.paths[0][:path]),
144 :revision => revision.paths[0][:revision],
145 :revision => revision.paths[0][:revision],
145 :branch => revision.paths[0][:branch]
146 :branch => revision.paths[0][:branch]
146 )
147 )
147 end
148 end
148 end
149 end
149
150
150 # Renumber new changesets in chronological order
151 # Renumber new changesets in chronological order
151 changesets.find(
152 changesets.find(
152 :all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE 'tmp%'"
153 :all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE 'tmp%'"
153 ).each do |changeset|
154 ).each do |changeset|
154 changeset.update_attribute :revision, next_revision_number
155 changeset.update_attribute :revision, next_revision_number
155 end
156 end
156 end # transaction
157 end # transaction
157 @current_revision_number = nil
158 @current_revision_number = nil
158 end
159 end
159
160
160 private
161 private
161
162
162 # Returns the next revision number to assign to a CVS changeset
163 # Returns the next revision number to assign to a CVS changeset
163 def next_revision_number
164 def next_revision_number
164 # Need to retrieve existing revision numbers to sort them as integers
165 # Need to retrieve existing revision numbers to sort them as integers
165 sql = "SELECT revision FROM #{Changeset.table_name} "
166 sql = "SELECT revision FROM #{Changeset.table_name} "
166 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
167 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
167 @current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
168 @current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
168 @current_revision_number += 1
169 @current_revision_number += 1
169 end
170 end
170 end
171 end
@@ -1,107 +1,111
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'redmine/scm/adapters/mercurial_adapter'
18 require 'redmine/scm/adapters/mercurial_adapter'
19
19
20 class Repository::Mercurial < Repository
20 class Repository::Mercurial < Repository
21 # sort changesets by revision number
21 # sort changesets by revision number
22 has_many :changesets, :order => "#{Changeset.table_name}.id DESC", :foreign_key => 'repository_id'
22 has_many :changesets, :order => "#{Changeset.table_name}.id DESC", :foreign_key => 'repository_id'
23
23
24 attr_protected :root_url
24 attr_protected :root_url
25 validates_presence_of :url
25 validates_presence_of :url
26
26
27 FETCH_AT_ONCE = 100 # number of changesets to fetch at once
27 FETCH_AT_ONCE = 100 # number of changesets to fetch at once
28
28
29 def self.scm_adapter_class
29 def self.scm_adapter_class
30 Redmine::Scm::Adapters::MercurialAdapter
30 Redmine::Scm::Adapters::MercurialAdapter
31 end
31 end
32
32
33 def self.scm_name
33 def self.scm_name
34 'Mercurial'
34 'Mercurial'
35 end
35 end
36
36
37 def repo_log_encoding
38 'UTF-8'
39 end
40
37 # Returns the readable identifier for the given mercurial changeset
41 # Returns the readable identifier for the given mercurial changeset
38 def self.format_changeset_identifier(changeset)
42 def self.format_changeset_identifier(changeset)
39 "#{changeset.revision}:#{changeset.scmid}"
43 "#{changeset.revision}:#{changeset.scmid}"
40 end
44 end
41
45
42 # Returns the identifier for the given Mercurial changeset
46 # Returns the identifier for the given Mercurial changeset
43 def self.changeset_identifier(changeset)
47 def self.changeset_identifier(changeset)
44 changeset.scmid
48 changeset.scmid
45 end
49 end
46
50
47 def branches
51 def branches
48 nil
52 nil
49 end
53 end
50
54
51 def tags
55 def tags
52 nil
56 nil
53 end
57 end
54
58
55 def diff_format_revisions(cs, cs_to, sep=':')
59 def diff_format_revisions(cs, cs_to, sep=':')
56 super(cs, cs_to, ' ')
60 super(cs, cs_to, ' ')
57 end
61 end
58
62
59 # Finds and returns a revision with a number or the beginning of a hash
63 # Finds and returns a revision with a number or the beginning of a hash
60 def find_changeset_by_name(name)
64 def find_changeset_by_name(name)
61 return nil if name.nil? || name.empty?
65 return nil if name.nil? || name.empty?
62 if /[^\d]/ =~ name or name.to_s.size > 8
66 if /[^\d]/ =~ name or name.to_s.size > 8
63 e = changesets.find(:first, :conditions => ['scmid = ?', name.to_s])
67 e = changesets.find(:first, :conditions => ['scmid = ?', name.to_s])
64 else
68 else
65 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
69 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
66 end
70 end
67 return e if e
71 return e if e
68 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) # last ditch
72 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) # last ditch
69 end
73 end
70
74
71 # Returns the latest changesets for +path+; sorted by revision number
75 # Returns the latest changesets for +path+; sorted by revision number
72 # Default behavior is to search in cached changesets
76 # Default behavior is to search in cached changesets
73 def latest_changesets(path, rev, limit=10)
77 def latest_changesets(path, rev, limit=10)
74 if path.blank?
78 if path.blank?
75 changesets.find(:all, :include => :user, :limit => limit)
79 changesets.find(:all, :include => :user, :limit => limit)
76 else
80 else
77 changesets.find(:all, :select => "DISTINCT #{Changeset.table_name}.*",
81 changesets.find(:all, :select => "DISTINCT #{Changeset.table_name}.*",
78 :joins => :changes,
82 :joins => :changes,
79 :conditions => ["#{Change.table_name}.path = ? OR #{Change.table_name}.path LIKE ? ESCAPE ?",
83 :conditions => ["#{Change.table_name}.path = ? OR #{Change.table_name}.path LIKE ? ESCAPE ?",
80 path.with_leading_slash,
84 path.with_leading_slash,
81 "#{path.with_leading_slash.gsub(/[%_\\]/) { |s| "\\#{s}" }}/%", '\\'],
85 "#{path.with_leading_slash.gsub(/[%_\\]/) { |s| "\\#{s}" }}/%", '\\'],
82 :include => :user, :limit => limit)
86 :include => :user, :limit => limit)
83 end
87 end
84 end
88 end
85
89
86 def fetch_changesets
90 def fetch_changesets
87 scm_rev = scm.info.lastrev.revision.to_i
91 scm_rev = scm.info.lastrev.revision.to_i
88 db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
92 db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
89 return unless db_rev < scm_rev # already up-to-date
93 return unless db_rev < scm_rev # already up-to-date
90
94
91 logger.debug "Fetching changesets for repository #{url}" if logger
95 logger.debug "Fetching changesets for repository #{url}" if logger
92 (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
96 (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
93 transaction do
97 transaction do
94 scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
98 scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
95 cs = Changeset.create(:repository => self,
99 cs = Changeset.create(:repository => self,
96 :revision => re.revision,
100 :revision => re.revision,
97 :scmid => re.scmid,
101 :scmid => re.scmid,
98 :committer => re.author,
102 :committer => re.author,
99 :committed_on => re.time,
103 :committed_on => re.time,
100 :comments => re.message)
104 :comments => re.message)
101 re.paths.each { |e| cs.create_change(e) }
105 re.paths.each { |e| cs.create_change(e) }
102 end
106 end
103 end
107 end
104 end
108 end
105 self
109 self
106 end
110 end
107 end
111 end
@@ -1,85 +1,89
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'redmine/scm/adapters/subversion_adapter'
18 require 'redmine/scm/adapters/subversion_adapter'
19
19
20 class Repository::Subversion < Repository
20 class Repository::Subversion < Repository
21 attr_protected :root_url
21 attr_protected :root_url
22 validates_presence_of :url
22 validates_presence_of :url
23 validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
23 validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
24
24
25 def self.scm_adapter_class
25 def self.scm_adapter_class
26 Redmine::Scm::Adapters::SubversionAdapter
26 Redmine::Scm::Adapters::SubversionAdapter
27 end
27 end
28
28
29 def self.scm_name
29 def self.scm_name
30 'Subversion'
30 'Subversion'
31 end
31 end
32
32
33 def repo_log_encoding
34 'UTF-8'
35 end
36
33 def latest_changesets(path, rev, limit=10)
37 def latest_changesets(path, rev, limit=10)
34 revisions = scm.revisions(path, rev, nil, :limit => limit)
38 revisions = scm.revisions(path, rev, nil, :limit => limit)
35 revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
39 revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
36 end
40 end
37
41
38 # Returns a path relative to the url of the repository
42 # Returns a path relative to the url of the repository
39 def relative_path(path)
43 def relative_path(path)
40 path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
44 path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
41 end
45 end
42
46
43 def fetch_changesets
47 def fetch_changesets
44 scm_info = scm.info
48 scm_info = scm.info
45 if scm_info
49 if scm_info
46 # latest revision found in database
50 # latest revision found in database
47 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
51 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
48 # latest revision in the repository
52 # latest revision in the repository
49 scm_revision = scm_info.lastrev.identifier.to_i
53 scm_revision = scm_info.lastrev.identifier.to_i
50 if db_revision < scm_revision
54 if db_revision < scm_revision
51 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
55 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
52 identifier_from = db_revision + 1
56 identifier_from = db_revision + 1
53 while (identifier_from <= scm_revision)
57 while (identifier_from <= scm_revision)
54 # loads changesets by batches of 200
58 # loads changesets by batches of 200
55 identifier_to = [identifier_from + 199, scm_revision].min
59 identifier_to = [identifier_from + 199, scm_revision].min
56 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
60 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
57 revisions.reverse_each do |revision|
61 revisions.reverse_each do |revision|
58 transaction do
62 transaction do
59 changeset = Changeset.create(:repository => self,
63 changeset = Changeset.create(:repository => self,
60 :revision => revision.identifier,
64 :revision => revision.identifier,
61 :committer => revision.author,
65 :committer => revision.author,
62 :committed_on => revision.time,
66 :committed_on => revision.time,
63 :comments => revision.message)
67 :comments => revision.message)
64
68
65 revision.paths.each do |change|
69 revision.paths.each do |change|
66 changeset.create_change(change)
70 changeset.create_change(change)
67 end unless changeset.new_record?
71 end unless changeset.new_record?
68 end
72 end
69 end unless revisions.nil?
73 end unless revisions.nil?
70 identifier_from = identifier_to + 1
74 identifier_from = identifier_to + 1
71 end
75 end
72 end
76 end
73 end
77 end
74 end
78 end
75
79
76 private
80 private
77
81
78 # Returns the relative url of the repository
82 # Returns the relative url of the repository
79 # Eg: root_url = file:///var/svn/foo
83 # Eg: root_url = file:///var/svn/foo
80 # url = file:///var/svn/foo/bar
84 # url = file:///var/svn/foo/bar
81 # => returns /bar
85 # => returns /bar
82 def relative_url
86 def relative_url
83 @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
87 @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
84 end
88 end
85 end
89 end
@@ -1,144 +1,166
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositorySubversionTest < ActiveSupport::TestCase
20 class RepositorySubversionTest < ActiveSupport::TestCase
21 fixtures :projects, :repositories, :enabled_modules, :users, :roles
21 fixtures :projects, :repositories, :enabled_modules, :users, :roles
22
22
23 def setup
23 def setup
24 @project = Project.find(3)
24 @project = Project.find(3)
25 assert @repository = Repository::Subversion.create(:project => @project,
25 assert @repository = Repository::Subversion.create(:project => @project,
26 :url => "file://#{self.class.repository_path('subversion')}")
26 :url => "file://#{self.class.repository_path('subversion')}")
27 end
27 end
28
28
29 if repository_configured?('subversion')
29 if repository_configured?('subversion')
30 def test_fetch_changesets_from_scratch
30 def test_fetch_changesets_from_scratch
31 @repository.fetch_changesets
31 @repository.fetch_changesets
32 @repository.reload
32 @repository.reload
33
33
34 assert_equal 11, @repository.changesets.count
34 assert_equal 11, @repository.changesets.count
35 assert_equal 20, @repository.changes.count
35 assert_equal 20, @repository.changes.count
36 assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments
36 assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments
37 end
37 end
38
38
39 def test_fetch_changesets_incremental
39 def test_fetch_changesets_incremental
40 @repository.fetch_changesets
40 @repository.fetch_changesets
41 # Remove changesets with revision > 5
41 # Remove changesets with revision > 5
42 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5}
42 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5}
43 @repository.reload
43 @repository.reload
44 assert_equal 5, @repository.changesets.count
44 assert_equal 5, @repository.changesets.count
45
45
46 @repository.fetch_changesets
46 @repository.fetch_changesets
47 assert_equal 11, @repository.changesets.count
47 assert_equal 11, @repository.changesets.count
48 end
48 end
49
49
50 def test_latest_changesets
50 def test_latest_changesets
51 @repository.fetch_changesets
51 @repository.fetch_changesets
52
52
53 # with limit
53 # with limit
54 changesets = @repository.latest_changesets('', nil, 2)
54 changesets = @repository.latest_changesets('', nil, 2)
55 assert_equal 2, changesets.size
55 assert_equal 2, changesets.size
56 assert_equal @repository.latest_changesets('', nil).slice(0,2), changesets
56 assert_equal @repository.latest_changesets('', nil).slice(0,2), changesets
57
57
58 # with path
58 # with path
59 changesets = @repository.latest_changesets('subversion_test/folder', nil)
59 changesets = @repository.latest_changesets('subversion_test/folder', nil)
60 assert_equal ["10", "9", "7", "6", "5", "2"], changesets.collect(&:revision)
60 assert_equal ["10", "9", "7", "6", "5", "2"], changesets.collect(&:revision)
61
61
62 # with path and revision
62 # with path and revision
63 changesets = @repository.latest_changesets('subversion_test/folder', 8)
63 changesets = @repository.latest_changesets('subversion_test/folder', 8)
64 assert_equal ["7", "6", "5", "2"], changesets.collect(&:revision)
64 assert_equal ["7", "6", "5", "2"], changesets.collect(&:revision)
65 end
65 end
66
66
67 def test_directory_listing_with_square_brackets_in_path
67 def test_directory_listing_with_square_brackets_in_path
68 @repository.fetch_changesets
68 @repository.fetch_changesets
69 @repository.reload
69 @repository.reload
70
70
71 entries = @repository.entries('subversion_test/[folder_with_brackets]')
71 entries = @repository.entries('subversion_test/[folder_with_brackets]')
72 assert_not_nil entries, 'Expect to find entries in folder_with_brackets'
72 assert_not_nil entries, 'Expect to find entries in folder_with_brackets'
73 assert_equal 1, entries.size, 'Expect one entry in folder_with_brackets'
73 assert_equal 1, entries.size, 'Expect one entry in folder_with_brackets'
74 assert_equal 'README.txt', entries.first.name
74 assert_equal 'README.txt', entries.first.name
75 end
75 end
76
76
77 def test_directory_listing_with_square_brackets_in_base
77 def test_directory_listing_with_square_brackets_in_base
78 @project = Project.find(3)
78 @project = Project.find(3)
79 @repository = Repository::Subversion.create(:project => @project, :url => "file:///#{self.class.repository_path('subversion')}/subversion_test/[folder_with_brackets]")
79 @repository = Repository::Subversion.create(:project => @project, :url => "file:///#{self.class.repository_path('subversion')}/subversion_test/[folder_with_brackets]")
80
80
81 @repository.fetch_changesets
81 @repository.fetch_changesets
82 @repository.reload
82 @repository.reload
83
83
84 assert_equal 1, @repository.changesets.count, 'Expected to see 1 revision'
84 assert_equal 1, @repository.changesets.count, 'Expected to see 1 revision'
85 assert_equal 2, @repository.changes.count, 'Expected to see 2 changes, dir add and file add'
85 assert_equal 2, @repository.changes.count, 'Expected to see 2 changes, dir add and file add'
86
86
87 entries = @repository.entries('')
87 entries = @repository.entries('')
88 assert_not_nil entries, 'Expect to find entries'
88 assert_not_nil entries, 'Expect to find entries'
89 assert_equal 1, entries.size, 'Expect a single entry'
89 assert_equal 1, entries.size, 'Expect a single entry'
90 assert_equal 'README.txt', entries.first.name
90 assert_equal 'README.txt', entries.first.name
91 end
91 end
92
92
93 def test_identifier
93 def test_identifier
94 @repository.fetch_changesets
94 @repository.fetch_changesets
95 @repository.reload
95 @repository.reload
96 c = @repository.changesets.find_by_revision('1')
96 c = @repository.changesets.find_by_revision('1')
97 assert_equal c.revision, c.identifier
97 assert_equal c.revision, c.identifier
98 end
98 end
99
99
100 def test_find_changeset_by_empty_name
100 def test_find_changeset_by_empty_name
101 @repository.fetch_changesets
101 @repository.fetch_changesets
102 @repository.reload
102 @repository.reload
103 ['', ' ', nil].each do |r|
103 ['', ' ', nil].each do |r|
104 assert_nil @repository.find_changeset_by_name(r)
104 assert_nil @repository.find_changeset_by_name(r)
105 end
105 end
106 end
106 end
107
107
108 def test_identifier_nine_digit
108 def test_identifier_nine_digit
109 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
109 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
110 :revision => '123456789', :comments => 'test')
110 :revision => '123456789', :comments => 'test')
111 assert_equal c.identifier, c.revision
111 assert_equal c.identifier, c.revision
112 end
112 end
113
113
114 def test_format_identifier
114 def test_format_identifier
115 @repository.fetch_changesets
115 @repository.fetch_changesets
116 @repository.reload
116 @repository.reload
117 c = @repository.changesets.find_by_revision('1')
117 c = @repository.changesets.find_by_revision('1')
118 assert_equal c.format_identifier, c.revision
118 assert_equal c.format_identifier, c.revision
119 end
119 end
120
120
121 def test_format_identifier_nine_digit
121 def test_format_identifier_nine_digit
122 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
122 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
123 :revision => '123456789', :comments => 'test')
123 :revision => '123456789', :comments => 'test')
124 assert_equal c.format_identifier, c.revision
124 assert_equal c.format_identifier, c.revision
125 end
125 end
126
126
127 def test_activities
127 def test_activities
128 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
128 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
129 :revision => '1', :comments => 'test')
129 :revision => '1', :comments => 'test')
130 assert c.event_title.include?('1:')
130 assert c.event_title.include?('1:')
131 assert_equal '1', c.event_url[:rev]
131 assert_equal '1', c.event_url[:rev]
132 end
132 end
133
133
134 def test_activities_nine_digit
134 def test_activities_nine_digit
135 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
135 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
136 :revision => '123456789', :comments => 'test')
136 :revision => '123456789', :comments => 'test')
137 assert c.event_title.include?('123456789:')
137 assert c.event_title.include?('123456789:')
138 assert_equal '123456789', c.event_url[:rev]
138 assert_equal '123456789', c.event_url[:rev]
139 end
139 end
140
141 def test_log_encoding_ignore_setting
142 with_settings :commit_logs_encoding => 'windows-1252' do
143 s1 = "\xC2\x80"
144 s2 = "\xc3\x82\xc2\x80"
145 if s1.respond_to?(:force_encoding)
146 s3 = s1
147 s4 = s2
148 s1.force_encoding('ASCII-8BIT')
149 s2.force_encoding('ASCII-8BIT')
150 s3.force_encoding('ISO-8859-1')
151 s4.force_encoding('UTF-8')
152 assert_equal s3.encode('UTF-8'), s4
153 end
154 c = Changeset.new(:repository => @repository,
155 :comments=>s2,
156 :revision=>'123',
157 :committed_on => Time.now)
158 assert c.save
159 assert_equal s2, c.comments
160 end
161 end
140 else
162 else
141 puts "Subversion test repository NOT FOUND. Skipping unit tests !!!"
163 puts "Subversion test repository NOT FOUND. Skipping unit tests !!!"
142 def test_fake; assert true end
164 def test_fake; assert true end
143 end
165 end
144 end
166 end
General Comments 0
You need to be logged in to leave comments. Login now