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