##// END OF EJS Templates
Move some RepositoriesController logic to Repository#stats_by_author (#13487)....
Jean-Baptiste Barth -
r12997:41bf39df3655
parent child
Show More
@@ -1,438 +1,439
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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'
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.where(:id => additional_user_ids).all unless additional_user_ids.empty?
97 @users += User.where(:id => additional_user_ids).all 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 @project.active? && Setting.autofetch_changesets? && @path.empty?
114 @repository.fetch_changesets if @project.active? && 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_id = params[:issue_id].to_s.sub(/^#/,'')
232 issue_id = params[:issue_id].to_s.sub(/^#/,'')
233 @issue = @changeset.find_referenced_issue_by_id(issue_id)
233 @issue = @changeset.find_referenced_issue_by_id(issue_id)
234 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
234 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
235 @issue = nil
235 @issue = nil
236 end
236 end
237
237
238 if @issue
238 if @issue
239 @changeset.issues << @issue
239 @changeset.issues << @issue
240 end
240 end
241 end
241 end
242
242
243 # Removes a related issue from a changeset
243 # Removes a related issue from a changeset
244 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
244 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
245 def remove_related_issue
245 def remove_related_issue
246 @issue = Issue.visible.find_by_id(params[:issue_id])
246 @issue = Issue.visible.find_by_id(params[:issue_id])
247 if @issue
247 if @issue
248 @changeset.issues.delete(@issue)
248 @changeset.issues.delete(@issue)
249 end
249 end
250 end
250 end
251
251
252 def diff
252 def diff
253 if params[:format] == 'diff'
253 if params[:format] == 'diff'
254 @diff = @repository.diff(@path, @rev, @rev_to)
254 @diff = @repository.diff(@path, @rev, @rev_to)
255 (show_error_not_found; return) unless @diff
255 (show_error_not_found; return) unless @diff
256 filename = "changeset_r#{@rev}"
256 filename = "changeset_r#{@rev}"
257 filename << "_r#{@rev_to}" if @rev_to
257 filename << "_r#{@rev_to}" if @rev_to
258 send_data @diff.join, :filename => "#{filename}.diff",
258 send_data @diff.join, :filename => "#{filename}.diff",
259 :type => 'text/x-patch',
259 :type => 'text/x-patch',
260 :disposition => 'attachment'
260 :disposition => 'attachment'
261 else
261 else
262 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
262 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
263 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
263 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
264
264
265 # Save diff type as user preference
265 # Save diff type as user preference
266 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
266 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
267 User.current.pref[:diff_type] = @diff_type
267 User.current.pref[:diff_type] = @diff_type
268 User.current.preference.save
268 User.current.preference.save
269 end
269 end
270 @cache_key = "repositories/diff/#{@repository.id}/" +
270 @cache_key = "repositories/diff/#{@repository.id}/" +
271 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
271 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
272 unless read_fragment(@cache_key)
272 unless read_fragment(@cache_key)
273 @diff = @repository.diff(@path, @rev, @rev_to)
273 @diff = @repository.diff(@path, @rev, @rev_to)
274 show_error_not_found unless @diff
274 show_error_not_found unless @diff
275 end
275 end
276
276
277 @changeset = @repository.find_changeset_by_name(@rev)
277 @changeset = @repository.find_changeset_by_name(@rev)
278 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
278 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
279 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
279 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
280 end
280 end
281 end
281 end
282
282
283 def stats
283 def stats
284 end
284 end
285
285
286 def graph
286 def graph
287 data = nil
287 data = nil
288 case params[:graph]
288 case params[:graph]
289 when "commits_per_month"
289 when "commits_per_month"
290 data = graph_commits_per_month(@repository)
290 data = graph_commits_per_month(@repository)
291 when "commits_per_author"
291 when "commits_per_author"
292 data = graph_commits_per_author(@repository)
292 data = graph_commits_per_author(@repository)
293 end
293 end
294 if data
294 if data
295 headers["Content-Type"] = "image/svg+xml"
295 headers["Content-Type"] = "image/svg+xml"
296 send_data(data, :type => "image/svg+xml", :disposition => "inline")
296 send_data(data, :type => "image/svg+xml", :disposition => "inline")
297 else
297 else
298 render_404
298 render_404
299 end
299 end
300 end
300 end
301
301
302 private
302 private
303
303
304 def find_repository
304 def find_repository
305 @repository = Repository.find(params[:id])
305 @repository = Repository.find(params[:id])
306 @project = @repository.project
306 @project = @repository.project
307 rescue ActiveRecord::RecordNotFound
307 rescue ActiveRecord::RecordNotFound
308 render_404
308 render_404
309 end
309 end
310
310
311 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
311 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
312
312
313 def find_project_repository
313 def find_project_repository
314 @project = Project.find(params[:id])
314 @project = Project.find(params[:id])
315 if params[:repository_id].present?
315 if params[:repository_id].present?
316 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
316 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
317 else
317 else
318 @repository = @project.repository
318 @repository = @project.repository
319 end
319 end
320 (render_404; return false) unless @repository
320 (render_404; return false) unless @repository
321 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
321 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
322 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
322 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
323 @rev_to = params[:rev_to]
323 @rev_to = params[:rev_to]
324
324
325 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
325 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
326 if @repository.branches.blank?
326 if @repository.branches.blank?
327 raise InvalidRevisionParam
327 raise InvalidRevisionParam
328 end
328 end
329 end
329 end
330 rescue ActiveRecord::RecordNotFound
330 rescue ActiveRecord::RecordNotFound
331 render_404
331 render_404
332 rescue InvalidRevisionParam
332 rescue InvalidRevisionParam
333 show_error_not_found
333 show_error_not_found
334 end
334 end
335
335
336 def find_changeset
336 def find_changeset
337 if @rev.present?
337 if @rev.present?
338 @changeset = @repository.find_changeset_by_name(@rev)
338 @changeset = @repository.find_changeset_by_name(@rev)
339 end
339 end
340 show_error_not_found unless @changeset
340 show_error_not_found unless @changeset
341 end
341 end
342
342
343 def show_error_not_found
343 def show_error_not_found
344 render_error :message => l(:error_scm_not_found), :status => 404
344 render_error :message => l(:error_scm_not_found), :status => 404
345 end
345 end
346
346
347 # Handler for Redmine::Scm::Adapters::CommandFailed exception
347 # Handler for Redmine::Scm::Adapters::CommandFailed exception
348 def show_error_command_failed(exception)
348 def show_error_command_failed(exception)
349 render_error l(:error_scm_command_failed, exception.message)
349 render_error l(:error_scm_command_failed, exception.message)
350 end
350 end
351
351
352 def graph_commits_per_month(repository)
352 def graph_commits_per_month(repository)
353 @date_to = Date.today
353 @date_to = Date.today
354 @date_from = @date_to << 11
354 @date_from = @date_to << 11
355 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
355 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
356 commits_by_day = Changeset.
356 commits_by_day = Changeset.
357 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
357 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
358 group(:commit_date).
358 group(:commit_date).
359 count
359 count
360 commits_by_month = [0] * 12
360 commits_by_month = [0] * 12
361 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
361 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
362
362
363 changes_by_day = Change.
363 changes_by_day = Change.
364 joins(:changeset).
364 joins(:changeset).
365 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
365 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
366 group(:commit_date).
366 group(:commit_date).
367 count
367 count
368 changes_by_month = [0] * 12
368 changes_by_month = [0] * 12
369 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
369 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
370
370
371 fields = []
371 fields = []
372 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
372 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
373
373
374 graph = SVG::Graph::Bar.new(
374 graph = SVG::Graph::Bar.new(
375 :height => 300,
375 :height => 300,
376 :width => 800,
376 :width => 800,
377 :fields => fields.reverse,
377 :fields => fields.reverse,
378 :stack => :side,
378 :stack => :side,
379 :scale_integers => true,
379 :scale_integers => true,
380 :step_x_labels => 2,
380 :step_x_labels => 2,
381 :show_data_values => false,
381 :show_data_values => false,
382 :graph_title => l(:label_commits_per_month),
382 :graph_title => l(:label_commits_per_month),
383 :show_graph_title => true
383 :show_graph_title => true
384 )
384 )
385
385
386 graph.add_data(
386 graph.add_data(
387 :data => commits_by_month[0..11].reverse,
387 :data => commits_by_month[0..11].reverse,
388 :title => l(:label_revision_plural)
388 :title => l(:label_revision_plural)
389 )
389 )
390
390
391 graph.add_data(
391 graph.add_data(
392 :data => changes_by_month[0..11].reverse,
392 :data => changes_by_month[0..11].reverse,
393 :title => l(:label_change_plural)
393 :title => l(:label_change_plural)
394 )
394 )
395
395
396 graph.burn
396 graph.burn
397 end
397 end
398
398
399 def graph_commits_per_author(repository)
399 def graph_commits_per_author(repository)
400 commits_by_author = Changeset.where("repository_id = ?", repository.id).group(:committer).count
400 #data
401 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
401 stats = repository.stats_by_author
402
402 fields, commits_data, changes_data = [], [], []
403 changes_by_author = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", repository.id).group(:committer).count
403 stats.each do |name, hsh|
404 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
404 fields << name
405
405 commits_data << hsh[:commits_count]
406 fields = commits_by_author.collect {|r| r.first}
406 changes_data << hsh[:changes_count]
407 commits_data = commits_by_author.collect {|r| r.last}
407 end
408 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
409
408
409 #expand to 10 values if needed
410 fields = fields + [""]*(10 - fields.length) if fields.length<10
410 fields = fields + [""]*(10 - fields.length) if fields.length<10
411 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
411 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
412 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
412 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
413
413
414 # Remove email address in usernames
414 # Remove email address in usernames
415 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
415 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
416
416
417 #prepare graph
417 graph = SVG::Graph::BarHorizontal.new(
418 graph = SVG::Graph::BarHorizontal.new(
418 :height => 30 * commits_data.length,
419 :height => 30 * commits_data.length,
419 :width => 800,
420 :width => 800,
420 :fields => fields,
421 :fields => fields,
421 :stack => :side,
422 :stack => :side,
422 :scale_integers => true,
423 :scale_integers => true,
423 :show_data_values => false,
424 :show_data_values => false,
424 :rotate_y_labels => false,
425 :rotate_y_labels => false,
425 :graph_title => l(:label_commits_per_author),
426 :graph_title => l(:label_commits_per_author),
426 :show_graph_title => true
427 :show_graph_title => true
427 )
428 )
428 graph.add_data(
429 graph.add_data(
429 :data => commits_data,
430 :data => commits_data,
430 :title => l(:label_revision_plural)
431 :title => l(:label_revision_plural)
431 )
432 )
432 graph.add_data(
433 graph.add_data(
433 :data => changes_data,
434 :data => changes_data,
434 :title => l(:label_change_plural)
435 :title => l(:label_change_plural)
435 )
436 )
436 graph.burn
437 graph.burn
437 end
438 end
438 end
439 end
@@ -1,447 +1,470
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ScmFetchError < Exception; end
18 class ScmFetchError < Exception; end
19
19
20 class Repository < ActiveRecord::Base
20 class Repository < ActiveRecord::Base
21 include Redmine::Ciphering
21 include Redmine::Ciphering
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23
23
24 # Maximum length for repository identifiers
24 # Maximum length for repository identifiers
25 IDENTIFIER_MAX_LENGTH = 255
25 IDENTIFIER_MAX_LENGTH = 255
26
26
27 belongs_to :project
27 belongs_to :project
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30
30
31 serialize :extra_info
31 serialize :extra_info
32
32
33 before_save :check_default
33 before_save :check_default
34
34
35 # Raw SQL to delete changesets and changes in the database
35 # Raw SQL to delete changesets and changes in the database
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 before_destroy :clear_changesets
37 before_destroy :clear_changesets
38
38
39 validates_length_of :password, :maximum => 255, :allow_nil => true
39 validates_length_of :password, :maximum => 255, :allow_nil => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
44 # donwcase letters, digits, dashes, underscores but not digits only
44 # donwcase letters, digits, dashes, underscores but not digits only
45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
46 # Checks if the SCM is enabled when creating a repository
46 # Checks if the SCM is enabled when creating a repository
47 validate :repo_create_validation, :on => :create
47 validate :repo_create_validation, :on => :create
48
48
49 safe_attributes 'identifier',
49 safe_attributes 'identifier',
50 'login',
50 'login',
51 'password',
51 'password',
52 'path_encoding',
52 'path_encoding',
53 'log_encoding',
53 'log_encoding',
54 'is_default'
54 'is_default'
55
55
56 safe_attributes 'url',
56 safe_attributes 'url',
57 :if => lambda {|repository, user| repository.new_record?}
57 :if => lambda {|repository, user| repository.new_record?}
58
58
59 def repo_create_validation
59 def repo_create_validation
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 errors.add(:type, :invalid)
61 errors.add(:type, :invalid)
62 end
62 end
63 end
63 end
64
64
65 def self.human_attribute_name(attribute_key_name, *args)
65 def self.human_attribute_name(attribute_key_name, *args)
66 attr_name = attribute_key_name.to_s
66 attr_name = attribute_key_name.to_s
67 if attr_name == "log_encoding"
67 if attr_name == "log_encoding"
68 attr_name = "commit_logs_encoding"
68 attr_name = "commit_logs_encoding"
69 end
69 end
70 super(attr_name, *args)
70 super(attr_name, *args)
71 end
71 end
72
72
73 # Removes leading and trailing whitespace
73 # Removes leading and trailing whitespace
74 def url=(arg)
74 def url=(arg)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 end
76 end
77
77
78 # Removes leading and trailing whitespace
78 # Removes leading and trailing whitespace
79 def root_url=(arg)
79 def root_url=(arg)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 end
81 end
82
82
83 def password
83 def password
84 read_ciphered_attribute(:password)
84 read_ciphered_attribute(:password)
85 end
85 end
86
86
87 def password=(arg)
87 def password=(arg)
88 write_ciphered_attribute(:password, arg)
88 write_ciphered_attribute(:password, arg)
89 end
89 end
90
90
91 def scm_adapter
91 def scm_adapter
92 self.class.scm_adapter_class
92 self.class.scm_adapter_class
93 end
93 end
94
94
95 def scm
95 def scm
96 unless @scm
96 unless @scm
97 @scm = self.scm_adapter.new(url, root_url,
97 @scm = self.scm_adapter.new(url, root_url,
98 login, password, path_encoding)
98 login, password, path_encoding)
99 if root_url.blank? && @scm.root_url.present?
99 if root_url.blank? && @scm.root_url.present?
100 update_attribute(:root_url, @scm.root_url)
100 update_attribute(:root_url, @scm.root_url)
101 end
101 end
102 end
102 end
103 @scm
103 @scm
104 end
104 end
105
105
106 def scm_name
106 def scm_name
107 self.class.scm_name
107 self.class.scm_name
108 end
108 end
109
109
110 def name
110 def name
111 if identifier.present?
111 if identifier.present?
112 identifier
112 identifier
113 elsif is_default?
113 elsif is_default?
114 l(:field_repository_is_default)
114 l(:field_repository_is_default)
115 else
115 else
116 scm_name
116 scm_name
117 end
117 end
118 end
118 end
119
119
120 def identifier=(identifier)
120 def identifier=(identifier)
121 super unless identifier_frozen?
121 super unless identifier_frozen?
122 end
122 end
123
123
124 def identifier_frozen?
124 def identifier_frozen?
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 end
126 end
127
127
128 def identifier_param
128 def identifier_param
129 if is_default?
129 if is_default?
130 nil
130 nil
131 elsif identifier.present?
131 elsif identifier.present?
132 identifier
132 identifier
133 else
133 else
134 id.to_s
134 id.to_s
135 end
135 end
136 end
136 end
137
137
138 def <=>(repository)
138 def <=>(repository)
139 if is_default?
139 if is_default?
140 -1
140 -1
141 elsif repository.is_default?
141 elsif repository.is_default?
142 1
142 1
143 else
143 else
144 identifier.to_s <=> repository.identifier.to_s
144 identifier.to_s <=> repository.identifier.to_s
145 end
145 end
146 end
146 end
147
147
148 def self.find_by_identifier_param(param)
148 def self.find_by_identifier_param(param)
149 if param.to_s =~ /^\d+$/
149 if param.to_s =~ /^\d+$/
150 find_by_id(param)
150 find_by_id(param)
151 else
151 else
152 find_by_identifier(param)
152 find_by_identifier(param)
153 end
153 end
154 end
154 end
155
155
156 # TODO: should return an empty hash instead of nil to avoid many ||{}
156 # TODO: should return an empty hash instead of nil to avoid many ||{}
157 def extra_info
157 def extra_info
158 h = read_attribute(:extra_info)
158 h = read_attribute(:extra_info)
159 h.is_a?(Hash) ? h : nil
159 h.is_a?(Hash) ? h : nil
160 end
160 end
161
161
162 def merge_extra_info(arg)
162 def merge_extra_info(arg)
163 h = extra_info || {}
163 h = extra_info || {}
164 return h if arg.nil?
164 return h if arg.nil?
165 h.merge!(arg)
165 h.merge!(arg)
166 write_attribute(:extra_info, h)
166 write_attribute(:extra_info, h)
167 end
167 end
168
168
169 def report_last_commit
169 def report_last_commit
170 true
170 true
171 end
171 end
172
172
173 def supports_cat?
173 def supports_cat?
174 scm.supports_cat?
174 scm.supports_cat?
175 end
175 end
176
176
177 def supports_annotate?
177 def supports_annotate?
178 scm.supports_annotate?
178 scm.supports_annotate?
179 end
179 end
180
180
181 def supports_all_revisions?
181 def supports_all_revisions?
182 true
182 true
183 end
183 end
184
184
185 def supports_directory_revisions?
185 def supports_directory_revisions?
186 false
186 false
187 end
187 end
188
188
189 def supports_revision_graph?
189 def supports_revision_graph?
190 false
190 false
191 end
191 end
192
192
193 def entry(path=nil, identifier=nil)
193 def entry(path=nil, identifier=nil)
194 scm.entry(path, identifier)
194 scm.entry(path, identifier)
195 end
195 end
196
196
197 def scm_entries(path=nil, identifier=nil)
197 def scm_entries(path=nil, identifier=nil)
198 scm.entries(path, identifier)
198 scm.entries(path, identifier)
199 end
199 end
200 protected :scm_entries
200 protected :scm_entries
201
201
202 def entries(path=nil, identifier=nil)
202 def entries(path=nil, identifier=nil)
203 entries = scm_entries(path, identifier)
203 entries = scm_entries(path, identifier)
204 load_entries_changesets(entries)
204 load_entries_changesets(entries)
205 entries
205 entries
206 end
206 end
207
207
208 def branches
208 def branches
209 scm.branches
209 scm.branches
210 end
210 end
211
211
212 def tags
212 def tags
213 scm.tags
213 scm.tags
214 end
214 end
215
215
216 def default_branch
216 def default_branch
217 nil
217 nil
218 end
218 end
219
219
220 def properties(path, identifier=nil)
220 def properties(path, identifier=nil)
221 scm.properties(path, identifier)
221 scm.properties(path, identifier)
222 end
222 end
223
223
224 def cat(path, identifier=nil)
224 def cat(path, identifier=nil)
225 scm.cat(path, identifier)
225 scm.cat(path, identifier)
226 end
226 end
227
227
228 def diff(path, rev, rev_to)
228 def diff(path, rev, rev_to)
229 scm.diff(path, rev, rev_to)
229 scm.diff(path, rev, rev_to)
230 end
230 end
231
231
232 def diff_format_revisions(cs, cs_to, sep=':')
232 def diff_format_revisions(cs, cs_to, sep=':')
233 text = ""
233 text = ""
234 text << cs_to.format_identifier + sep if cs_to
234 text << cs_to.format_identifier + sep if cs_to
235 text << cs.format_identifier if cs
235 text << cs.format_identifier if cs
236 text
236 text
237 end
237 end
238
238
239 # Returns a path relative to the url of the repository
239 # Returns a path relative to the url of the repository
240 def relative_path(path)
240 def relative_path(path)
241 path
241 path
242 end
242 end
243
243
244 # Finds and returns a revision with a number or the beginning of a hash
244 # Finds and returns a revision with a number or the beginning of a hash
245 def find_changeset_by_name(name)
245 def find_changeset_by_name(name)
246 return nil if name.blank?
246 return nil if name.blank?
247 s = name.to_s
247 s = name.to_s
248 if s.match(/^\d*$/)
248 if s.match(/^\d*$/)
249 changesets.where("revision = ?", s).first
249 changesets.where("revision = ?", s).first
250 else
250 else
251 changesets.where("revision LIKE ?", s + '%').first
251 changesets.where("revision LIKE ?", s + '%').first
252 end
252 end
253 end
253 end
254
254
255 def latest_changeset
255 def latest_changeset
256 @latest_changeset ||= changesets.first
256 @latest_changeset ||= changesets.first
257 end
257 end
258
258
259 # Returns the latest changesets for +path+
259 # Returns the latest changesets for +path+
260 # Default behaviour is to search in cached changesets
260 # Default behaviour is to search in cached changesets
261 def latest_changesets(path, rev, limit=10)
261 def latest_changesets(path, rev, limit=10)
262 if path.blank?
262 if path.blank?
263 changesets.
263 changesets.
264 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
264 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
265 limit(limit).
265 limit(limit).
266 preload(:user).
266 preload(:user).
267 all
267 all
268 else
268 else
269 filechanges.
269 filechanges.
270 where("path = ?", path.with_leading_slash).
270 where("path = ?", path.with_leading_slash).
271 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
271 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
272 limit(limit).
272 limit(limit).
273 preload(:changeset => :user).
273 preload(:changeset => :user).
274 collect(&:changeset)
274 collect(&:changeset)
275 end
275 end
276 end
276 end
277
277
278 def scan_changesets_for_issue_ids
278 def scan_changesets_for_issue_ids
279 self.changesets.each(&:scan_comment_for_issue_ids)
279 self.changesets.each(&:scan_comment_for_issue_ids)
280 end
280 end
281
281
282 # Returns an array of committers usernames and associated user_id
282 # Returns an array of committers usernames and associated user_id
283 def committers
283 def committers
284 @committers ||= Changeset.connection.select_rows(
284 @committers ||= Changeset.connection.select_rows(
285 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
285 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
286 end
286 end
287
287
288 # Maps committers username to a user ids
288 # Maps committers username to a user ids
289 def committer_ids=(h)
289 def committer_ids=(h)
290 if h.is_a?(Hash)
290 if h.is_a?(Hash)
291 committers.each do |committer, user_id|
291 committers.each do |committer, user_id|
292 new_user_id = h[committer]
292 new_user_id = h[committer]
293 if new_user_id && (new_user_id.to_i != user_id.to_i)
293 if new_user_id && (new_user_id.to_i != user_id.to_i)
294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
297 end
297 end
298 end
298 end
299 @committers = nil
299 @committers = nil
300 @found_committer_users = nil
300 @found_committer_users = nil
301 true
301 true
302 else
302 else
303 false
303 false
304 end
304 end
305 end
305 end
306
306
307 # Returns the Redmine User corresponding to the given +committer+
307 # Returns the Redmine User corresponding to the given +committer+
308 # It will return nil if the committer is not yet mapped and if no User
308 # It will return nil if the committer is not yet mapped and if no User
309 # with the same username or email was found
309 # with the same username or email was found
310 def find_committer_user(committer)
310 def find_committer_user(committer)
311 unless committer.blank?
311 unless committer.blank?
312 @found_committer_users ||= {}
312 @found_committer_users ||= {}
313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
314
314
315 user = nil
315 user = nil
316 c = changesets.where(:committer => committer).includes(:user).first
316 c = changesets.where(:committer => committer).includes(:user).first
317 if c && c.user
317 if c && c.user
318 user = c.user
318 user = c.user
319 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
319 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
320 username, email = $1.strip, $3
320 username, email = $1.strip, $3
321 u = User.find_by_login(username)
321 u = User.find_by_login(username)
322 u ||= User.find_by_mail(email) unless email.blank?
322 u ||= User.find_by_mail(email) unless email.blank?
323 user = u
323 user = u
324 end
324 end
325 @found_committer_users[committer] = user
325 @found_committer_users[committer] = user
326 user
326 user
327 end
327 end
328 end
328 end
329
329
330 def repo_log_encoding
330 def repo_log_encoding
331 encoding = log_encoding.to_s.strip
331 encoding = log_encoding.to_s.strip
332 encoding.blank? ? 'UTF-8' : encoding
332 encoding.blank? ? 'UTF-8' : encoding
333 end
333 end
334
334
335 # Fetches new changesets for all repositories of active projects
335 # Fetches new changesets for all repositories of active projects
336 # Can be called periodically by an external script
336 # Can be called periodically by an external script
337 # eg. ruby script/runner "Repository.fetch_changesets"
337 # eg. ruby script/runner "Repository.fetch_changesets"
338 def self.fetch_changesets
338 def self.fetch_changesets
339 Project.active.has_module(:repository).all.each do |project|
339 Project.active.has_module(:repository).all.each do |project|
340 project.repositories.each do |repository|
340 project.repositories.each do |repository|
341 begin
341 begin
342 repository.fetch_changesets
342 repository.fetch_changesets
343 rescue Redmine::Scm::Adapters::CommandFailed => e
343 rescue Redmine::Scm::Adapters::CommandFailed => e
344 logger.error "scm: error during fetching changesets: #{e.message}"
344 logger.error "scm: error during fetching changesets: #{e.message}"
345 end
345 end
346 end
346 end
347 end
347 end
348 end
348 end
349
349
350 # scan changeset comments to find related and fixed issues for all repositories
350 # scan changeset comments to find related and fixed issues for all repositories
351 def self.scan_changesets_for_issue_ids
351 def self.scan_changesets_for_issue_ids
352 all.each(&:scan_changesets_for_issue_ids)
352 all.each(&:scan_changesets_for_issue_ids)
353 end
353 end
354
354
355 def self.scm_name
355 def self.scm_name
356 'Abstract'
356 'Abstract'
357 end
357 end
358
358
359 def self.available_scm
359 def self.available_scm
360 subclasses.collect {|klass| [klass.scm_name, klass.name]}
360 subclasses.collect {|klass| [klass.scm_name, klass.name]}
361 end
361 end
362
362
363 def self.factory(klass_name, *args)
363 def self.factory(klass_name, *args)
364 klass = "Repository::#{klass_name}".constantize
364 klass = "Repository::#{klass_name}".constantize
365 klass.new(*args)
365 klass.new(*args)
366 rescue
366 rescue
367 nil
367 nil
368 end
368 end
369
369
370 def self.scm_adapter_class
370 def self.scm_adapter_class
371 nil
371 nil
372 end
372 end
373
373
374 def self.scm_command
374 def self.scm_command
375 ret = ""
375 ret = ""
376 begin
376 begin
377 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
377 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
378 rescue Exception => e
378 rescue Exception => e
379 logger.error "scm: error during get command: #{e.message}"
379 logger.error "scm: error during get command: #{e.message}"
380 end
380 end
381 ret
381 ret
382 end
382 end
383
383
384 def self.scm_version_string
384 def self.scm_version_string
385 ret = ""
385 ret = ""
386 begin
386 begin
387 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
387 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
388 rescue Exception => e
388 rescue Exception => e
389 logger.error "scm: error during get version string: #{e.message}"
389 logger.error "scm: error during get version string: #{e.message}"
390 end
390 end
391 ret
391 ret
392 end
392 end
393
393
394 def self.scm_available
394 def self.scm_available
395 ret = false
395 ret = false
396 begin
396 begin
397 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
397 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
398 rescue Exception => e
398 rescue Exception => e
399 logger.error "scm: error during get scm available: #{e.message}"
399 logger.error "scm: error during get scm available: #{e.message}"
400 end
400 end
401 ret
401 ret
402 end
402 end
403
403
404 def set_as_default?
404 def set_as_default?
405 new_record? && project && Repository.where(:project_id => project.id).empty?
405 new_record? && project && Repository.where(:project_id => project.id).empty?
406 end
406 end
407
407
408 # Returns a hash with statistics by author in the following form:
409 # {
410 # "John Smith" => { :commits => 45, :changes => 324 },
411 # "Bob" => { ... }
412 # }
413 #
414 # Notes:
415 # - this hash honnors the users mapping defined for the repository
416 def stats_by_author
417 commits_by_author = Changeset.where("repository_id = ?", id).group(:committer).count
418 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
419
420 changes_by_author = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).group(:committer).count
421 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
422
423 commits_by_author.inject({}) do |hash, (name, commits_count)|
424 hash[name] = {}
425 hash[name][:commits_count] = commits_count
426 hash[name][:changes_count] = h[name] || 0
427 hash
428 end
429 end
430
408 protected
431 protected
409
432
410 def check_default
433 def check_default
411 if !is_default? && set_as_default?
434 if !is_default? && set_as_default?
412 self.is_default = true
435 self.is_default = true
413 end
436 end
414 if is_default? && is_default_changed?
437 if is_default? && is_default_changed?
415 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
438 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
416 end
439 end
417 end
440 end
418
441
419 def load_entries_changesets(entries)
442 def load_entries_changesets(entries)
420 if entries
443 if entries
421 entries.each do |entry|
444 entries.each do |entry|
422 if entry.lastrev && entry.lastrev.identifier
445 if entry.lastrev && entry.lastrev.identifier
423 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
446 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
424 end
447 end
425 end
448 end
426 end
449 end
427 end
450 end
428
451
429 private
452 private
430
453
431 # Deletes repository data
454 # Deletes repository data
432 def clear_changesets
455 def clear_changesets
433 cs = Changeset.table_name
456 cs = Changeset.table_name
434 ch = Change.table_name
457 ch = Change.table_name
435 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
458 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
436 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
459 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
437
460
438 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
461 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
439 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
462 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
440 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
463 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
441 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
464 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
442 clear_extra_info_of_changesets
465 clear_extra_info_of_changesets
443 end
466 end
444
467
445 def clear_extra_info_of_changesets
468 def clear_extra_info_of_changesets
446 end
469 end
447 end
470 end
@@ -1,398 +1,417
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositoryTest < ActiveSupport::TestCase
20 class RepositoryTest < ActiveSupport::TestCase
21 fixtures :projects,
21 fixtures :projects,
22 :trackers,
22 :trackers,
23 :projects_trackers,
23 :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :repositories,
25 :repositories,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :issue_categories,
28 :issue_categories,
29 :changesets,
29 :changesets,
30 :changes,
30 :changes,
31 :users,
31 :users,
32 :members,
32 :members,
33 :member_roles,
33 :member_roles,
34 :roles,
34 :roles,
35 :enumerations
35 :enumerations
36
36
37 include Redmine::I18n
37 include Redmine::I18n
38
38
39 def setup
39 def setup
40 @repository = Project.find(1).repository
40 @repository = Project.find(1).repository
41 end
41 end
42
42
43 def test_blank_log_encoding_error_message
43 def test_blank_log_encoding_error_message
44 set_language_if_valid 'en'
44 set_language_if_valid 'en'
45 repo = Repository::Bazaar.new(
45 repo = Repository::Bazaar.new(
46 :project => Project.find(3),
46 :project => Project.find(3),
47 :url => "/test",
47 :url => "/test",
48 :log_encoding => ''
48 :log_encoding => ''
49 )
49 )
50 assert !repo.save
50 assert !repo.save
51 assert_include "Commit messages encoding can't be blank",
51 assert_include "Commit messages encoding can't be blank",
52 repo.errors.full_messages
52 repo.errors.full_messages
53 end
53 end
54
54
55 def test_blank_log_encoding_error_message_fr
55 def test_blank_log_encoding_error_message_fr
56 set_language_if_valid 'fr'
56 set_language_if_valid 'fr'
57 str = "Encodage des messages de commit doit \xc3\xaatre renseign\xc3\xa9(e)"
57 str = "Encodage des messages de commit doit \xc3\xaatre renseign\xc3\xa9(e)"
58 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
58 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
59 repo = Repository::Bazaar.new(
59 repo = Repository::Bazaar.new(
60 :project => Project.find(3),
60 :project => Project.find(3),
61 :url => "/test"
61 :url => "/test"
62 )
62 )
63 assert !repo.save
63 assert !repo.save
64 assert_include str, repo.errors.full_messages
64 assert_include str, repo.errors.full_messages
65 end
65 end
66
66
67 def test_create
67 def test_create
68 repository = Repository::Subversion.new(:project => Project.find(3))
68 repository = Repository::Subversion.new(:project => Project.find(3))
69 assert !repository.save
69 assert !repository.save
70
70
71 repository.url = "svn://localhost"
71 repository.url = "svn://localhost"
72 assert repository.save
72 assert repository.save
73 repository.reload
73 repository.reload
74
74
75 project = Project.find(3)
75 project = Project.find(3)
76 assert_equal repository, project.repository
76 assert_equal repository, project.repository
77 end
77 end
78
78
79 def test_first_repository_should_be_set_as_default
79 def test_first_repository_should_be_set_as_default
80 repository1 = Repository::Subversion.new(
80 repository1 = Repository::Subversion.new(
81 :project => Project.find(3),
81 :project => Project.find(3),
82 :identifier => 'svn1',
82 :identifier => 'svn1',
83 :url => 'file:///svn1'
83 :url => 'file:///svn1'
84 )
84 )
85 assert repository1.save
85 assert repository1.save
86 assert repository1.is_default?
86 assert repository1.is_default?
87
87
88 repository2 = Repository::Subversion.new(
88 repository2 = Repository::Subversion.new(
89 :project => Project.find(3),
89 :project => Project.find(3),
90 :identifier => 'svn2',
90 :identifier => 'svn2',
91 :url => 'file:///svn2'
91 :url => 'file:///svn2'
92 )
92 )
93 assert repository2.save
93 assert repository2.save
94 assert !repository2.is_default?
94 assert !repository2.is_default?
95
95
96 assert_equal repository1, Project.find(3).repository
96 assert_equal repository1, Project.find(3).repository
97 assert_equal [repository1, repository2], Project.find(3).repositories.sort
97 assert_equal [repository1, repository2], Project.find(3).repositories.sort
98 end
98 end
99
99
100 def test_default_repository_should_be_one
100 def test_default_repository_should_be_one
101 assert_equal 0, Project.find(3).repositories.count
101 assert_equal 0, Project.find(3).repositories.count
102 repository1 = Repository::Subversion.new(
102 repository1 = Repository::Subversion.new(
103 :project => Project.find(3),
103 :project => Project.find(3),
104 :identifier => 'svn1',
104 :identifier => 'svn1',
105 :url => 'file:///svn1'
105 :url => 'file:///svn1'
106 )
106 )
107 assert repository1.save
107 assert repository1.save
108 assert repository1.is_default?
108 assert repository1.is_default?
109
109
110 repository2 = Repository::Subversion.new(
110 repository2 = Repository::Subversion.new(
111 :project => Project.find(3),
111 :project => Project.find(3),
112 :identifier => 'svn2',
112 :identifier => 'svn2',
113 :url => 'file:///svn2',
113 :url => 'file:///svn2',
114 :is_default => true
114 :is_default => true
115 )
115 )
116 assert repository2.save
116 assert repository2.save
117 assert repository2.is_default?
117 assert repository2.is_default?
118 repository1.reload
118 repository1.reload
119 assert !repository1.is_default?
119 assert !repository1.is_default?
120
120
121 assert_equal repository2, Project.find(3).repository
121 assert_equal repository2, Project.find(3).repository
122 assert_equal [repository2, repository1], Project.find(3).repositories.sort
122 assert_equal [repository2, repository1], Project.find(3).repositories.sort
123 end
123 end
124
124
125 def test_identifier_should_accept_letters_digits_dashes_and_underscores
125 def test_identifier_should_accept_letters_digits_dashes_and_underscores
126 r = Repository::Subversion.new(
126 r = Repository::Subversion.new(
127 :project_id => 3,
127 :project_id => 3,
128 :identifier => 'svn-123_45',
128 :identifier => 'svn-123_45',
129 :url => 'file:///svn'
129 :url => 'file:///svn'
130 )
130 )
131 assert r.save
131 assert r.save
132 end
132 end
133
133
134 def test_identifier_should_not_be_frozen_for_a_new_repository
134 def test_identifier_should_not_be_frozen_for_a_new_repository
135 assert_equal false, Repository.new.identifier_frozen?
135 assert_equal false, Repository.new.identifier_frozen?
136 end
136 end
137
137
138 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
138 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
139 Repository.where(:id => 10).update_all(["identifier = ''"])
139 Repository.where(:id => 10).update_all(["identifier = ''"])
140 assert_equal false, Repository.find(10).identifier_frozen?
140 assert_equal false, Repository.find(10).identifier_frozen?
141 end
141 end
142
142
143 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
143 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
144 Repository.where(:id => 10).update_all(["identifier = 'abc123'"])
144 Repository.where(:id => 10).update_all(["identifier = 'abc123'"])
145 assert_equal true, Repository.find(10).identifier_frozen?
145 assert_equal true, Repository.find(10).identifier_frozen?
146 end
146 end
147
147
148 def test_identifier_should_not_accept_change_if_frozen
148 def test_identifier_should_not_accept_change_if_frozen
149 r = Repository.new(:identifier => 'foo')
149 r = Repository.new(:identifier => 'foo')
150 r.stubs(:identifier_frozen?).returns(true)
150 r.stubs(:identifier_frozen?).returns(true)
151
151
152 r.identifier = 'bar'
152 r.identifier = 'bar'
153 assert_equal 'foo', r.identifier
153 assert_equal 'foo', r.identifier
154 end
154 end
155
155
156 def test_identifier_should_accept_change_if_not_frozen
156 def test_identifier_should_accept_change_if_not_frozen
157 r = Repository.new(:identifier => 'foo')
157 r = Repository.new(:identifier => 'foo')
158 r.stubs(:identifier_frozen?).returns(false)
158 r.stubs(:identifier_frozen?).returns(false)
159
159
160 r.identifier = 'bar'
160 r.identifier = 'bar'
161 assert_equal 'bar', r.identifier
161 assert_equal 'bar', r.identifier
162 end
162 end
163
163
164 def test_destroy
164 def test_destroy
165 repository = Repository.find(10)
165 repository = Repository.find(10)
166 changesets = repository.changesets.count
166 changesets = repository.changesets.count
167 changes = repository.filechanges.count
167 changes = repository.filechanges.count
168
168
169 assert_difference 'Changeset.count', -changesets do
169 assert_difference 'Changeset.count', -changesets do
170 assert_difference 'Change.count', -changes do
170 assert_difference 'Change.count', -changes do
171 Repository.find(10).destroy
171 Repository.find(10).destroy
172 end
172 end
173 end
173 end
174 end
174 end
175
175
176 def test_destroy_should_delete_parents_associations
176 def test_destroy_should_delete_parents_associations
177 changeset = Changeset.find(102)
177 changeset = Changeset.find(102)
178 changeset.parents = Changeset.where(:id => [100, 101]).all
178 changeset.parents = Changeset.where(:id => [100, 101]).all
179 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").count', -2 do
179 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").count', -2 do
180 Repository.find(10).destroy
180 Repository.find(10).destroy
181 end
181 end
182 end
182 end
183
183
184 def test_destroy_should_delete_issues_associations
184 def test_destroy_should_delete_issues_associations
185 changeset = Changeset.find(102)
185 changeset = Changeset.find(102)
186 changeset.issues = Issue.where(:id => [1, 2]).all
186 changeset.issues = Issue.where(:id => [1, 2]).all
187 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").count', -2 do
187 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").count', -2 do
188 Repository.find(10).destroy
188 Repository.find(10).destroy
189 end
189 end
190 end
190 end
191
191
192 def test_should_not_create_with_disabled_scm
192 def test_should_not_create_with_disabled_scm
193 # disable Subversion
193 # disable Subversion
194 with_settings :enabled_scm => ['Darcs', 'Git'] do
194 with_settings :enabled_scm => ['Darcs', 'Git'] do
195 repository = Repository::Subversion.new(
195 repository = Repository::Subversion.new(
196 :project => Project.find(3), :url => "svn://localhost")
196 :project => Project.find(3), :url => "svn://localhost")
197 assert !repository.save
197 assert !repository.save
198 assert_include I18n.translate('activerecord.errors.messages.invalid'),
198 assert_include I18n.translate('activerecord.errors.messages.invalid'),
199 repository.errors[:type]
199 repository.errors[:type]
200 end
200 end
201 end
201 end
202
202
203 def test_scan_changesets_for_issue_ids
203 def test_scan_changesets_for_issue_ids
204 Setting.default_language = 'en'
204 Setting.default_language = 'en'
205 Setting.commit_ref_keywords = 'refs , references, IssueID'
205 Setting.commit_ref_keywords = 'refs , references, IssueID'
206 Setting.commit_update_keywords = [
206 Setting.commit_update_keywords = [
207 {'keywords' => 'fixes , closes',
207 {'keywords' => 'fixes , closes',
208 'status_id' => IssueStatus.where(:is_closed => true).first.id,
208 'status_id' => IssueStatus.where(:is_closed => true).first.id,
209 'done_ratio' => '90'}
209 'done_ratio' => '90'}
210 ]
210 ]
211 Setting.default_language = 'en'
211 Setting.default_language = 'en'
212 ActionMailer::Base.deliveries.clear
212 ActionMailer::Base.deliveries.clear
213
213
214 # make sure issue 1 is not already closed
214 # make sure issue 1 is not already closed
215 fixed_issue = Issue.find(1)
215 fixed_issue = Issue.find(1)
216 assert !fixed_issue.status.is_closed?
216 assert !fixed_issue.status.is_closed?
217 old_status = fixed_issue.status
217 old_status = fixed_issue.status
218
218
219 with_settings :notified_events => %w(issue_added issue_updated) do
219 with_settings :notified_events => %w(issue_added issue_updated) do
220 Repository.scan_changesets_for_issue_ids
220 Repository.scan_changesets_for_issue_ids
221 end
221 end
222 assert_equal [101, 102], Issue.find(3).changeset_ids
222 assert_equal [101, 102], Issue.find(3).changeset_ids
223
223
224 # fixed issues
224 # fixed issues
225 fixed_issue.reload
225 fixed_issue.reload
226 assert fixed_issue.status.is_closed?
226 assert fixed_issue.status.is_closed?
227 assert_equal 90, fixed_issue.done_ratio
227 assert_equal 90, fixed_issue.done_ratio
228 assert_equal [101], fixed_issue.changeset_ids
228 assert_equal [101], fixed_issue.changeset_ids
229
229
230 # issue change
230 # issue change
231 journal = fixed_issue.journals.reorder('created_on desc').first
231 journal = fixed_issue.journals.reorder('created_on desc').first
232 assert_equal User.find_by_login('dlopper'), journal.user
232 assert_equal User.find_by_login('dlopper'), journal.user
233 assert_equal 'Applied in changeset r2.', journal.notes
233 assert_equal 'Applied in changeset r2.', journal.notes
234
234
235 # 2 email notifications
235 # 2 email notifications
236 assert_equal 2, ActionMailer::Base.deliveries.size
236 assert_equal 2, ActionMailer::Base.deliveries.size
237 mail = ActionMailer::Base.deliveries.first
237 mail = ActionMailer::Base.deliveries.first
238 assert_not_nil mail
238 assert_not_nil mail
239 assert mail.subject.starts_with?(
239 assert mail.subject.starts_with?(
240 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
240 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
241 assert_mail_body_match(
241 assert_mail_body_match(
242 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
242 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
243
243
244 # ignoring commits referencing an issue of another project
244 # ignoring commits referencing an issue of another project
245 assert_equal [], Issue.find(4).changesets
245 assert_equal [], Issue.find(4).changesets
246 end
246 end
247
247
248 def test_for_changeset_comments_strip
248 def test_for_changeset_comments_strip
249 repository = Repository::Mercurial.create(
249 repository = Repository::Mercurial.create(
250 :project => Project.find( 4 ),
250 :project => Project.find( 4 ),
251 :url => '/foo/bar/baz' )
251 :url => '/foo/bar/baz' )
252 comment = <<-COMMENT
252 comment = <<-COMMENT
253 This is a loooooooooooooooooooooooooooong comment
253 This is a loooooooooooooooooooooooooooong comment
254
254
255
255
256 COMMENT
256 COMMENT
257 changeset = Changeset.new(
257 changeset = Changeset.new(
258 :comments => comment, :commit_date => Time.now,
258 :comments => comment, :commit_date => Time.now,
259 :revision => 0, :scmid => 'f39b7922fb3c',
259 :revision => 0, :scmid => 'f39b7922fb3c',
260 :committer => 'foo <foo@example.com>',
260 :committer => 'foo <foo@example.com>',
261 :committed_on => Time.now, :repository => repository )
261 :committed_on => Time.now, :repository => repository )
262 assert( changeset.save )
262 assert( changeset.save )
263 assert_not_equal( comment, changeset.comments )
263 assert_not_equal( comment, changeset.comments )
264 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
264 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
265 changeset.comments )
265 changeset.comments )
266 end
266 end
267
267
268 def test_for_urls_strip_cvs
268 def test_for_urls_strip_cvs
269 repository = Repository::Cvs.create(
269 repository = Repository::Cvs.create(
270 :project => Project.find(4),
270 :project => Project.find(4),
271 :url => ' :pserver:login:password@host:/path/to/the/repository',
271 :url => ' :pserver:login:password@host:/path/to/the/repository',
272 :root_url => 'foo ',
272 :root_url => 'foo ',
273 :log_encoding => 'UTF-8')
273 :log_encoding => 'UTF-8')
274 assert repository.save
274 assert repository.save
275 repository.reload
275 repository.reload
276 assert_equal ':pserver:login:password@host:/path/to/the/repository',
276 assert_equal ':pserver:login:password@host:/path/to/the/repository',
277 repository.url
277 repository.url
278 assert_equal 'foo', repository.root_url
278 assert_equal 'foo', repository.root_url
279 end
279 end
280
280
281 def test_for_urls_strip_subversion
281 def test_for_urls_strip_subversion
282 repository = Repository::Subversion.create(
282 repository = Repository::Subversion.create(
283 :project => Project.find(4),
283 :project => Project.find(4),
284 :url => ' file:///dummy ')
284 :url => ' file:///dummy ')
285 assert repository.save
285 assert repository.save
286 repository.reload
286 repository.reload
287 assert_equal 'file:///dummy', repository.url
287 assert_equal 'file:///dummy', repository.url
288 end
288 end
289
289
290 def test_for_urls_strip_git
290 def test_for_urls_strip_git
291 repository = Repository::Git.create(
291 repository = Repository::Git.create(
292 :project => Project.find(4),
292 :project => Project.find(4),
293 :url => ' c:\dummy ')
293 :url => ' c:\dummy ')
294 assert repository.save
294 assert repository.save
295 repository.reload
295 repository.reload
296 assert_equal 'c:\dummy', repository.url
296 assert_equal 'c:\dummy', repository.url
297 end
297 end
298
298
299 def test_manual_user_mapping
299 def test_manual_user_mapping
300 assert_no_difference "Changeset.where('user_id <> 2').count" do
300 assert_no_difference "Changeset.where('user_id <> 2').count" do
301 c = Changeset.create!(
301 c = Changeset.create!(
302 :repository => @repository,
302 :repository => @repository,
303 :committer => 'foo',
303 :committer => 'foo',
304 :committed_on => Time.now,
304 :committed_on => Time.now,
305 :revision => 100,
305 :revision => 100,
306 :comments => 'Committed by foo.'
306 :comments => 'Committed by foo.'
307 )
307 )
308 assert_nil c.user
308 assert_nil c.user
309 @repository.committer_ids = {'foo' => '2'}
309 @repository.committer_ids = {'foo' => '2'}
310 assert_equal User.find(2), c.reload.user
310 assert_equal User.find(2), c.reload.user
311 # committer is now mapped
311 # committer is now mapped
312 c = Changeset.create!(
312 c = Changeset.create!(
313 :repository => @repository,
313 :repository => @repository,
314 :committer => 'foo',
314 :committer => 'foo',
315 :committed_on => Time.now,
315 :committed_on => Time.now,
316 :revision => 101,
316 :revision => 101,
317 :comments => 'Another commit by foo.'
317 :comments => 'Another commit by foo.'
318 )
318 )
319 assert_equal User.find(2), c.user
319 assert_equal User.find(2), c.user
320 end
320 end
321 end
321 end
322
322
323 def test_auto_user_mapping_by_username
323 def test_auto_user_mapping_by_username
324 c = Changeset.create!(
324 c = Changeset.create!(
325 :repository => @repository,
325 :repository => @repository,
326 :committer => 'jsmith',
326 :committer => 'jsmith',
327 :committed_on => Time.now,
327 :committed_on => Time.now,
328 :revision => 100,
328 :revision => 100,
329 :comments => 'Committed by john.'
329 :comments => 'Committed by john.'
330 )
330 )
331 assert_equal User.find(2), c.user
331 assert_equal User.find(2), c.user
332 end
332 end
333
333
334 def test_auto_user_mapping_by_email
334 def test_auto_user_mapping_by_email
335 c = Changeset.create!(
335 c = Changeset.create!(
336 :repository => @repository,
336 :repository => @repository,
337 :committer => 'john <jsmith@somenet.foo>',
337 :committer => 'john <jsmith@somenet.foo>',
338 :committed_on => Time.now,
338 :committed_on => Time.now,
339 :revision => 100,
339 :revision => 100,
340 :comments => 'Committed by john.'
340 :comments => 'Committed by john.'
341 )
341 )
342 assert_equal User.find(2), c.user
342 assert_equal User.find(2), c.user
343 end
343 end
344
344
345 def test_filesystem_avaialbe
345 def test_filesystem_avaialbe
346 klass = Repository::Filesystem
346 klass = Repository::Filesystem
347 assert klass.scm_adapter_class
347 assert klass.scm_adapter_class
348 assert_equal true, klass.scm_available
348 assert_equal true, klass.scm_available
349 end
349 end
350
350
351 def test_extra_info_should_not_return_non_hash_value
351 def test_extra_info_should_not_return_non_hash_value
352 repo = Repository.new
352 repo = Repository.new
353 repo.extra_info = "foo"
353 repo.extra_info = "foo"
354 assert_nil repo.extra_info
354 assert_nil repo.extra_info
355 end
355 end
356
356
357 def test_merge_extra_info
357 def test_merge_extra_info
358 repo = Repository::Subversion.new(:project => Project.find(3))
358 repo = Repository::Subversion.new(:project => Project.find(3))
359 assert !repo.save
359 assert !repo.save
360 repo.url = "svn://localhost"
360 repo.url = "svn://localhost"
361 assert repo.save
361 assert repo.save
362 repo.reload
362 repo.reload
363 project = Project.find(3)
363 project = Project.find(3)
364 assert_equal repo, project.repository
364 assert_equal repo, project.repository
365 assert_nil repo.extra_info
365 assert_nil repo.extra_info
366 h1 = {"test_1" => {"test_11" => "test_value_11"}}
366 h1 = {"test_1" => {"test_11" => "test_value_11"}}
367 repo.merge_extra_info(h1)
367 repo.merge_extra_info(h1)
368 assert_equal h1, repo.extra_info
368 assert_equal h1, repo.extra_info
369 h2 = {"test_2" => {
369 h2 = {"test_2" => {
370 "test_21" => "test_value_21",
370 "test_21" => "test_value_21",
371 "test_22" => "test_value_22",
371 "test_22" => "test_value_22",
372 }}
372 }}
373 repo.merge_extra_info(h2)
373 repo.merge_extra_info(h2)
374 assert_equal (h = {"test_11" => "test_value_11"}),
374 assert_equal (h = {"test_11" => "test_value_11"}),
375 repo.extra_info["test_1"]
375 repo.extra_info["test_1"]
376 assert_equal "test_value_21",
376 assert_equal "test_value_21",
377 repo.extra_info["test_2"]["test_21"]
377 repo.extra_info["test_2"]["test_21"]
378 h3 = {"test_2" => {
378 h3 = {"test_2" => {
379 "test_23" => "test_value_23",
379 "test_23" => "test_value_23",
380 "test_24" => "test_value_24",
380 "test_24" => "test_value_24",
381 }}
381 }}
382 repo.merge_extra_info(h3)
382 repo.merge_extra_info(h3)
383 assert_equal (h = {"test_11" => "test_value_11"}),
383 assert_equal (h = {"test_11" => "test_value_11"}),
384 repo.extra_info["test_1"]
384 repo.extra_info["test_1"]
385 assert_nil repo.extra_info["test_2"]["test_21"]
385 assert_nil repo.extra_info["test_2"]["test_21"]
386 assert_equal "test_value_23",
386 assert_equal "test_value_23",
387 repo.extra_info["test_2"]["test_23"]
387 repo.extra_info["test_2"]["test_23"]
388 end
388 end
389
389
390 def test_sort_should_not_raise_an_error_with_nil_identifiers
390 def test_sort_should_not_raise_an_error_with_nil_identifiers
391 r1 = Repository.new
391 r1 = Repository.new
392 r2 = Repository.new
392 r2 = Repository.new
393
393
394 assert_nothing_raised do
394 assert_nothing_raised do
395 [r1, r2].sort
395 [r1, r2].sort
396 end
396 end
397 end
397 end
398
399 def test_stats_by_author_reflect_changesets_and_changes
400 repository = Repository.find(10)
401
402 expected = {"dlopper"=>{:commits_count=>10, :changes_count=>3}}
403 assert_equal expected, repository.stats_by_author
404
405 set = Changeset.create!(
406 :repository => repository,
407 :committer => 'dlopper',
408 :committed_on => Time.now,
409 :revision => 101,
410 :comments => 'Another commit by foo.'
411 )
412 Change.create!(:changeset => set, :action => 'create', :path => '/path/to/file1')
413 Change.create!(:changeset => set, :action => 'create', :path => '/path/to/file2')
414 expected = {"dlopper"=>{:commits_count=>11, :changes_count=>5}}
415 assert_equal expected, repository.stats_by_author
416 end
398 end
417 end
General Comments 0
You need to be logged in to leave comments. Login now