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