##// END OF EJS Templates
Merged r12273 from trunk to 2.3-stable....
Toshi MARUYAMA -
r12118:765ee4c62d22
parent child
Show More
@@ -0,0 +1,25
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Scm
20 module Adapters
21 class CommandFailed < StandardError #:nodoc:
22 end
23 end
24 end
25 end
@@ -1,434 +1,434
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 require 'redmine/scm/adapters/abstract_adapter'
21 require 'redmine/scm/adapters'
22 22
23 23 class ChangesetNotFound < Exception; end
24 24 class InvalidRevisionParam < Exception; end
25 25
26 26 class RepositoriesController < ApplicationController
27 27 menu_item :repository
28 28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
29 29 default_search_scope :changesets
30 30
31 31 before_filter :find_project_by_project_id, :only => [:new, :create]
32 32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
33 33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 34 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
35 35 before_filter :authorize
36 36 accept_rss_auth :revisions
37 37
38 38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
39 39
40 40 def new
41 41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
42 42 @repository = Repository.factory(scm)
43 43 @repository.is_default = @project.repository.nil?
44 44 @repository.project = @project
45 45 end
46 46
47 47 def create
48 48 attrs = pickup_extra_info
49 49 @repository = Repository.factory(params[:repository_scm])
50 50 @repository.safe_attributes = params[:repository]
51 51 if attrs[:attrs_extra].keys.any?
52 52 @repository.merge_extra_info(attrs[:attrs_extra])
53 53 end
54 54 @repository.project = @project
55 55 if request.post? && @repository.save
56 56 redirect_to settings_project_path(@project, :tab => 'repositories')
57 57 else
58 58 render :action => 'new'
59 59 end
60 60 end
61 61
62 62 def edit
63 63 end
64 64
65 65 def update
66 66 attrs = pickup_extra_info
67 67 @repository.safe_attributes = attrs[:attrs]
68 68 if attrs[:attrs_extra].keys.any?
69 69 @repository.merge_extra_info(attrs[:attrs_extra])
70 70 end
71 71 @repository.project = @project
72 72 if request.put? && @repository.save
73 73 redirect_to settings_project_path(@project, :tab => 'repositories')
74 74 else
75 75 render :action => 'edit'
76 76 end
77 77 end
78 78
79 79 def pickup_extra_info
80 80 p = {}
81 81 p_extra = {}
82 82 params[:repository].each do |k, v|
83 83 if k =~ /^extra_/
84 84 p_extra[k] = v
85 85 else
86 86 p[k] = v
87 87 end
88 88 end
89 89 {:attrs => p, :attrs_extra => p_extra}
90 90 end
91 91 private :pickup_extra_info
92 92
93 93 def committers
94 94 @committers = @repository.committers
95 95 @users = @project.users
96 96 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
97 97 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
98 98 @users.compact!
99 99 @users.sort!
100 100 if request.post? && params[:committers].is_a?(Hash)
101 101 # Build a hash with repository usernames as keys and corresponding user ids as values
102 102 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
103 103 flash[:notice] = l(:notice_successful_update)
104 104 redirect_to settings_project_path(@project, :tab => 'repositories')
105 105 end
106 106 end
107 107
108 108 def destroy
109 109 @repository.destroy if request.delete?
110 110 redirect_to settings_project_path(@project, :tab => 'repositories')
111 111 end
112 112
113 113 def show
114 114 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
115 115
116 116 @entries = @repository.entries(@path, @rev)
117 117 @changeset = @repository.find_changeset_by_name(@rev)
118 118 if request.xhr?
119 119 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
120 120 else
121 121 (show_error_not_found; return) unless @entries
122 122 @changesets = @repository.latest_changesets(@path, @rev)
123 123 @properties = @repository.properties(@path, @rev)
124 124 @repositories = @project.repositories
125 125 render :action => 'show'
126 126 end
127 127 end
128 128
129 129 alias_method :browse, :show
130 130
131 131 def changes
132 132 @entry = @repository.entry(@path, @rev)
133 133 (show_error_not_found; return) unless @entry
134 134 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
135 135 @properties = @repository.properties(@path, @rev)
136 136 @changeset = @repository.find_changeset_by_name(@rev)
137 137 end
138 138
139 139 def revisions
140 140 @changeset_count = @repository.changesets.count
141 141 @changeset_pages = Paginator.new @changeset_count,
142 142 per_page_option,
143 143 params['page']
144 144 @changesets = @repository.changesets.
145 145 limit(@changeset_pages.per_page).
146 146 offset(@changeset_pages.offset).
147 147 includes(:user, :repository, :parents).
148 148 all
149 149
150 150 respond_to do |format|
151 151 format.html { render :layout => false if request.xhr? }
152 152 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
153 153 end
154 154 end
155 155
156 156 def raw
157 157 entry_and_raw(true)
158 158 end
159 159
160 160 def entry
161 161 entry_and_raw(false)
162 162 end
163 163
164 164 def entry_and_raw(is_raw)
165 165 @entry = @repository.entry(@path, @rev)
166 166 (show_error_not_found; return) unless @entry
167 167
168 168 # If the entry is a dir, show the browser
169 169 (show; return) if @entry.is_dir?
170 170
171 171 @content = @repository.cat(@path, @rev)
172 172 (show_error_not_found; return) unless @content
173 173 if is_raw ||
174 174 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
175 175 ! is_entry_text_data?(@content, @path)
176 176 # Force the download
177 177 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
178 178 send_type = Redmine::MimeType.of(@path)
179 179 send_opt[:type] = send_type.to_s if send_type
180 180 send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment')
181 181 send_data @content, send_opt
182 182 else
183 183 # Prevent empty lines when displaying a file with Windows style eol
184 184 # TODO: UTF-16
185 185 # Is this needs? AttachmentsController reads file simply.
186 186 @content.gsub!("\r\n", "\n")
187 187 @changeset = @repository.find_changeset_by_name(@rev)
188 188 end
189 189 end
190 190 private :entry_and_raw
191 191
192 192 def is_entry_text_data?(ent, path)
193 193 # UTF-16 contains "\x00".
194 194 # It is very strict that file contains less than 30% of ascii symbols
195 195 # in non Western Europe.
196 196 return true if Redmine::MimeType.is_type?('text', path)
197 197 # Ruby 1.8.6 has a bug of integer divisions.
198 198 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
199 199 return false if ent.is_binary_data?
200 200 true
201 201 end
202 202 private :is_entry_text_data?
203 203
204 204 def annotate
205 205 @entry = @repository.entry(@path, @rev)
206 206 (show_error_not_found; return) unless @entry
207 207
208 208 @annotate = @repository.scm.annotate(@path, @rev)
209 209 if @annotate.nil? || @annotate.empty?
210 210 (render_error l(:error_scm_annotate); return)
211 211 end
212 212 ann_buf_size = 0
213 213 @annotate.lines.each do |buf|
214 214 ann_buf_size += buf.size
215 215 end
216 216 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
217 217 (render_error l(:error_scm_annotate_big_text_file); return)
218 218 end
219 219 @changeset = @repository.find_changeset_by_name(@rev)
220 220 end
221 221
222 222 def revision
223 223 respond_to do |format|
224 224 format.html
225 225 format.js {render :layout => false}
226 226 end
227 227 end
228 228
229 229 # Adds a related issue to a changeset
230 230 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
231 231 def add_related_issue
232 232 @issue = @changeset.find_referenced_issue_by_id(params[:issue_id])
233 233 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
234 234 @issue = nil
235 235 end
236 236
237 237 if @issue
238 238 @changeset.issues << @issue
239 239 end
240 240 end
241 241
242 242 # Removes a related issue from a changeset
243 243 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
244 244 def remove_related_issue
245 245 @issue = Issue.visible.find_by_id(params[:issue_id])
246 246 if @issue
247 247 @changeset.issues.delete(@issue)
248 248 end
249 249 end
250 250
251 251 def diff
252 252 if params[:format] == 'diff'
253 253 @diff = @repository.diff(@path, @rev, @rev_to)
254 254 (show_error_not_found; return) unless @diff
255 255 filename = "changeset_r#{@rev}"
256 256 filename << "_r#{@rev_to}" if @rev_to
257 257 send_data @diff.join, :filename => "#{filename}.diff",
258 258 :type => 'text/x-patch',
259 259 :disposition => 'attachment'
260 260 else
261 261 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
262 262 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
263 263
264 264 # Save diff type as user preference
265 265 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
266 266 User.current.pref[:diff_type] = @diff_type
267 267 User.current.preference.save
268 268 end
269 269 @cache_key = "repositories/diff/#{@repository.id}/" +
270 270 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
271 271 unless read_fragment(@cache_key)
272 272 @diff = @repository.diff(@path, @rev, @rev_to)
273 273 show_error_not_found unless @diff
274 274 end
275 275
276 276 @changeset = @repository.find_changeset_by_name(@rev)
277 277 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
278 278 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
279 279 end
280 280 end
281 281
282 282 def stats
283 283 end
284 284
285 285 def graph
286 286 data = nil
287 287 case params[:graph]
288 288 when "commits_per_month"
289 289 data = graph_commits_per_month(@repository)
290 290 when "commits_per_author"
291 291 data = graph_commits_per_author(@repository)
292 292 end
293 293 if data
294 294 headers["Content-Type"] = "image/svg+xml"
295 295 send_data(data, :type => "image/svg+xml", :disposition => "inline")
296 296 else
297 297 render_404
298 298 end
299 299 end
300 300
301 301 private
302 302
303 303 def find_repository
304 304 @repository = Repository.find(params[:id])
305 305 @project = @repository.project
306 306 rescue ActiveRecord::RecordNotFound
307 307 render_404
308 308 end
309 309
310 310 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
311 311
312 312 def find_project_repository
313 313 @project = Project.find(params[:id])
314 314 if params[:repository_id].present?
315 315 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
316 316 else
317 317 @repository = @project.repository
318 318 end
319 319 (render_404; return false) unless @repository
320 320 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
321 321 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
322 322 @rev_to = params[:rev_to]
323 323
324 324 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
325 325 if @repository.branches.blank?
326 326 raise InvalidRevisionParam
327 327 end
328 328 end
329 329 rescue ActiveRecord::RecordNotFound
330 330 render_404
331 331 rescue InvalidRevisionParam
332 332 show_error_not_found
333 333 end
334 334
335 335 def find_changeset
336 336 if @rev.present?
337 337 @changeset = @repository.find_changeset_by_name(@rev)
338 338 end
339 339 show_error_not_found unless @changeset
340 340 end
341 341
342 342 def show_error_not_found
343 343 render_error :message => l(:error_scm_not_found), :status => 404
344 344 end
345 345
346 346 # Handler for Redmine::Scm::Adapters::CommandFailed exception
347 347 def show_error_command_failed(exception)
348 348 render_error l(:error_scm_command_failed, exception.message)
349 349 end
350 350
351 351 def graph_commits_per_month(repository)
352 352 @date_to = Date.today
353 353 @date_from = @date_to << 11
354 354 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
355 355 commits_by_day = Changeset.count(
356 356 :all, :group => :commit_date,
357 357 :conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
358 358 commits_by_month = [0] * 12
359 359 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
360 360
361 361 changes_by_day = Change.count(
362 362 :all, :group => :commit_date, :include => :changeset,
363 363 :conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
364 364 changes_by_month = [0] * 12
365 365 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
366 366
367 367 fields = []
368 368 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
369 369
370 370 graph = SVG::Graph::Bar.new(
371 371 :height => 300,
372 372 :width => 800,
373 373 :fields => fields.reverse,
374 374 :stack => :side,
375 375 :scale_integers => true,
376 376 :step_x_labels => 2,
377 377 :show_data_values => false,
378 378 :graph_title => l(:label_commits_per_month),
379 379 :show_graph_title => true
380 380 )
381 381
382 382 graph.add_data(
383 383 :data => commits_by_month[0..11].reverse,
384 384 :title => l(:label_revision_plural)
385 385 )
386 386
387 387 graph.add_data(
388 388 :data => changes_by_month[0..11].reverse,
389 389 :title => l(:label_change_plural)
390 390 )
391 391
392 392 graph.burn
393 393 end
394 394
395 395 def graph_commits_per_author(repository)
396 396 commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id])
397 397 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
398 398
399 399 changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id])
400 400 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
401 401
402 402 fields = commits_by_author.collect {|r| r.first}
403 403 commits_data = commits_by_author.collect {|r| r.last}
404 404 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
405 405
406 406 fields = fields + [""]*(10 - fields.length) if fields.length<10
407 407 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
408 408 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
409 409
410 410 # Remove email adress in usernames
411 411 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
412 412
413 413 graph = SVG::Graph::BarHorizontal.new(
414 414 :height => 400,
415 415 :width => 800,
416 416 :fields => fields,
417 417 :stack => :side,
418 418 :scale_integers => true,
419 419 :show_data_values => false,
420 420 :rotate_y_labels => false,
421 421 :graph_title => l(:label_commits_per_author),
422 422 :show_graph_title => true
423 423 )
424 424 graph.add_data(
425 425 :data => commits_data,
426 426 :title => l(:label_revision_plural)
427 427 )
428 428 graph.add_data(
429 429 :data => changes_data,
430 430 :title => l(:label_change_plural)
431 431 )
432 432 graph.burn
433 433 end
434 434 end
@@ -1,450 +1,447
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 'cgi'
19 19
20 20 if RUBY_VERSION < '1.9'
21 21 require 'iconv'
22 22 end
23 23
24 24 module Redmine
25 25 module Scm
26 26 module Adapters
27 class CommandFailed < StandardError #:nodoc:
28 end
29
30 27 class AbstractAdapter #:nodoc:
31 28
32 29 # raised if scm command exited with error, e.g. unknown revision.
33 30 class ScmCommandAborted < CommandFailed; end
34 31
35 32 class << self
36 33 def client_command
37 34 ""
38 35 end
39 36
40 37 def shell_quote_command
41 38 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
42 39 client_command
43 40 else
44 41 shell_quote(client_command)
45 42 end
46 43 end
47 44
48 45 # Returns the version of the scm client
49 46 # Eg: [1, 5, 0] or [] if unknown
50 47 def client_version
51 48 []
52 49 end
53 50
54 51 # Returns the version string of the scm client
55 52 # Eg: '1.5.0' or 'Unknown version' if unknown
56 53 def client_version_string
57 54 v = client_version || 'Unknown version'
58 55 v.is_a?(Array) ? v.join('.') : v.to_s
59 56 end
60 57
61 58 # Returns true if the current client version is above
62 59 # or equals the given one
63 60 # If option is :unknown is set to true, it will return
64 61 # true if the client version is unknown
65 62 def client_version_above?(v, options={})
66 63 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
67 64 end
68 65
69 66 def client_available
70 67 true
71 68 end
72 69
73 70 def shell_quote(str)
74 71 if Redmine::Platform.mswin?
75 72 '"' + str.gsub(/"/, '\\"') + '"'
76 73 else
77 74 "'" + str.gsub(/'/, "'\"'\"'") + "'"
78 75 end
79 76 end
80 77 end
81 78
82 79 def initialize(url, root_url=nil, login=nil, password=nil,
83 80 path_encoding=nil)
84 81 @url = url
85 82 @login = login if login && !login.empty?
86 83 @password = (password || "") if @login
87 84 @root_url = root_url.blank? ? retrieve_root_url : root_url
88 85 end
89 86
90 87 def adapter_name
91 88 'Abstract'
92 89 end
93 90
94 91 def supports_cat?
95 92 true
96 93 end
97 94
98 95 def supports_annotate?
99 96 respond_to?('annotate')
100 97 end
101 98
102 99 def root_url
103 100 @root_url
104 101 end
105 102
106 103 def url
107 104 @url
108 105 end
109 106
110 107 def path_encoding
111 108 nil
112 109 end
113 110
114 111 # get info about the svn repository
115 112 def info
116 113 return nil
117 114 end
118 115
119 116 # Returns the entry identified by path and revision identifier
120 117 # or nil if entry doesn't exist in the repository
121 118 def entry(path=nil, identifier=nil)
122 119 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
123 120 search_path = parts[0..-2].join('/')
124 121 search_name = parts[-1]
125 122 if search_path.blank? && search_name.blank?
126 123 # Root entry
127 124 Entry.new(:path => '', :kind => 'dir')
128 125 else
129 126 # Search for the entry in the parent directory
130 127 es = entries(search_path, identifier)
131 128 es ? es.detect {|e| e.name == search_name} : nil
132 129 end
133 130 end
134 131
135 132 # Returns an Entries collection
136 133 # or nil if the given path doesn't exist in the repository
137 134 def entries(path=nil, identifier=nil, options={})
138 135 return nil
139 136 end
140 137
141 138 def branches
142 139 return nil
143 140 end
144 141
145 142 def tags
146 143 return nil
147 144 end
148 145
149 146 def default_branch
150 147 return nil
151 148 end
152 149
153 150 def properties(path, identifier=nil)
154 151 return nil
155 152 end
156 153
157 154 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
158 155 return nil
159 156 end
160 157
161 158 def diff(path, identifier_from, identifier_to=nil)
162 159 return nil
163 160 end
164 161
165 162 def cat(path, identifier=nil)
166 163 return nil
167 164 end
168 165
169 166 def with_leading_slash(path)
170 167 path ||= ''
171 168 (path[0,1]!="/") ? "/#{path}" : path
172 169 end
173 170
174 171 def with_trailling_slash(path)
175 172 path ||= ''
176 173 (path[-1,1] == "/") ? path : "#{path}/"
177 174 end
178 175
179 176 def without_leading_slash(path)
180 177 path ||= ''
181 178 path.gsub(%r{^/+}, '')
182 179 end
183 180
184 181 def without_trailling_slash(path)
185 182 path ||= ''
186 183 (path[-1,1] == "/") ? path[0..-2] : path
187 184 end
188 185
189 186 def shell_quote(str)
190 187 self.class.shell_quote(str)
191 188 end
192 189
193 190 private
194 191 def retrieve_root_url
195 192 info = self.info
196 193 info ? info.root_url : nil
197 194 end
198 195
199 196 def target(path, sq=true)
200 197 path ||= ''
201 198 base = path.match(/^\//) ? root_url : url
202 199 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
203 200 if sq
204 201 str = shell_quote(str)
205 202 end
206 203 str
207 204 end
208 205
209 206 def logger
210 207 self.class.logger
211 208 end
212 209
213 210 def shellout(cmd, options = {}, &block)
214 211 self.class.shellout(cmd, options, &block)
215 212 end
216 213
217 214 def self.logger
218 215 Rails.logger
219 216 end
220 217
221 218 # Path to the file where scm stderr output is logged
222 219 # Returns nil if the log file is not writable
223 220 def self.stderr_log_file
224 221 if @stderr_log_file.nil?
225 222 writable = false
226 223 path = Redmine::Configuration['scm_stderr_log_file'].presence
227 224 path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
228 225 if File.exists?(path)
229 226 if File.file?(path) && File.writable?(path)
230 227 writable = true
231 228 else
232 229 logger.warn("SCM log file (#{path}) is not writable")
233 230 end
234 231 else
235 232 begin
236 233 File.open(path, "w") {}
237 234 writable = true
238 235 rescue => e
239 236 logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
240 237 end
241 238 end
242 239 @stderr_log_file = writable ? path : false
243 240 end
244 241 @stderr_log_file || nil
245 242 end
246 243
247 244 def self.shellout(cmd, options = {}, &block)
248 245 if logger && logger.debug?
249 246 logger.debug "Shelling out: #{strip_credential(cmd)}"
250 247 # Capture stderr in a log file
251 248 if stderr_log_file
252 249 cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
253 250 end
254 251 end
255 252 begin
256 253 mode = "r+"
257 254 IO.popen(cmd, mode) do |io|
258 255 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
259 256 io.close_write unless options[:write_stdin]
260 257 block.call(io) if block_given?
261 258 end
262 259 ## If scm command does not exist,
263 260 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
264 261 ## in production environment.
265 262 # rescue Errno::ENOENT => e
266 263 rescue Exception => e
267 264 msg = strip_credential(e.message)
268 265 # The command failed, log it and re-raise
269 266 logmsg = "SCM command failed, "
270 267 logmsg += "make sure that your SCM command (e.g. svn) is "
271 268 logmsg += "in PATH (#{ENV['PATH']})\n"
272 269 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
273 270 logmsg += "#{strip_credential(cmd)}\n"
274 271 logmsg += "with: #{msg}"
275 272 logger.error(logmsg)
276 273 raise CommandFailed.new(msg)
277 274 end
278 275 end
279 276
280 277 # Hides username/password in a given command
281 278 def self.strip_credential(cmd)
282 279 q = (Redmine::Platform.mswin? ? '"' : "'")
283 280 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
284 281 end
285 282
286 283 def strip_credential(cmd)
287 284 self.class.strip_credential(cmd)
288 285 end
289 286
290 287 def scm_iconv(to, from, str)
291 288 return nil if str.nil?
292 289 return str if to == from
293 290 if str.respond_to?(:force_encoding)
294 291 str.force_encoding(from)
295 292 begin
296 293 str.encode(to)
297 294 rescue Exception => err
298 295 logger.error("failed to convert from #{from} to #{to}. #{err}")
299 296 nil
300 297 end
301 298 else
302 299 begin
303 300 Iconv.conv(to, from, str)
304 301 rescue Iconv::Failure => err
305 302 logger.error("failed to convert from #{from} to #{to}. #{err}")
306 303 nil
307 304 end
308 305 end
309 306 end
310 307
311 308 def parse_xml(xml)
312 309 if RUBY_PLATFORM == 'java'
313 310 xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
314 311 end
315 312 ActiveSupport::XmlMini.parse(xml)
316 313 end
317 314 end
318 315
319 316 class Entries < Array
320 317 def sort_by_name
321 318 dup.sort! {|x,y|
322 319 if x.kind == y.kind
323 320 x.name.to_s <=> y.name.to_s
324 321 else
325 322 x.kind <=> y.kind
326 323 end
327 324 }
328 325 end
329 326
330 327 def revisions
331 328 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
332 329 end
333 330 end
334 331
335 332 class Info
336 333 attr_accessor :root_url, :lastrev
337 334 def initialize(attributes={})
338 335 self.root_url = attributes[:root_url] if attributes[:root_url]
339 336 self.lastrev = attributes[:lastrev]
340 337 end
341 338 end
342 339
343 340 class Entry
344 341 attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
345 342
346 343 def initialize(attributes={})
347 344 self.name = attributes[:name] if attributes[:name]
348 345 self.path = attributes[:path] if attributes[:path]
349 346 self.kind = attributes[:kind] if attributes[:kind]
350 347 self.size = attributes[:size].to_i if attributes[:size]
351 348 self.lastrev = attributes[:lastrev]
352 349 end
353 350
354 351 def is_file?
355 352 'file' == self.kind
356 353 end
357 354
358 355 def is_dir?
359 356 'dir' == self.kind
360 357 end
361 358
362 359 def is_text?
363 360 Redmine::MimeType.is_type?('text', name)
364 361 end
365 362
366 363 def author
367 364 if changeset
368 365 changeset.author.to_s
369 366 elsif lastrev
370 367 Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
371 368 end
372 369 end
373 370 end
374 371
375 372 class Revisions < Array
376 373 def latest
377 374 sort {|x,y|
378 375 unless x.time.nil? or y.time.nil?
379 376 x.time <=> y.time
380 377 else
381 378 0
382 379 end
383 380 }.last
384 381 end
385 382 end
386 383
387 384 class Revision
388 385 attr_accessor :scmid, :name, :author, :time, :message,
389 386 :paths, :revision, :branch, :identifier,
390 387 :parents
391 388
392 389 def initialize(attributes={})
393 390 self.identifier = attributes[:identifier]
394 391 self.scmid = attributes[:scmid]
395 392 self.name = attributes[:name] || self.identifier
396 393 self.author = attributes[:author]
397 394 self.time = attributes[:time]
398 395 self.message = attributes[:message] || ""
399 396 self.paths = attributes[:paths]
400 397 self.revision = attributes[:revision]
401 398 self.branch = attributes[:branch]
402 399 self.parents = attributes[:parents]
403 400 end
404 401
405 402 # Returns the readable identifier.
406 403 def format_identifier
407 404 self.identifier.to_s
408 405 end
409 406
410 407 def ==(other)
411 408 if other.nil?
412 409 false
413 410 elsif scmid.present?
414 411 scmid == other.scmid
415 412 elsif identifier.present?
416 413 identifier == other.identifier
417 414 elsif revision.present?
418 415 revision == other.revision
419 416 end
420 417 end
421 418 end
422 419
423 420 class Annotate
424 421 attr_reader :lines, :revisions
425 422
426 423 def initialize
427 424 @lines = []
428 425 @revisions = []
429 426 end
430 427
431 428 def add_line(line, revision)
432 429 @lines << line
433 430 @revisions << revision
434 431 end
435 432
436 433 def content
437 434 content = lines.join("\n")
438 435 end
439 436
440 437 def empty?
441 438 lines.empty?
442 439 end
443 440 end
444 441
445 442 class Branch < String
446 443 attr_accessor :revision, :scmid
447 444 end
448 445 end
449 446 end
450 447 end
General Comments 0
You need to be logged in to leave comments. Login now