##// END OF EJS Templates
Wider SVG graphs in repository stats....
Jean-Philippe Lang -
r1587:94cf4f258ff6
parent child
Show More
@@ -1,313 +1,313
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require '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
21
22 class ChangesetNotFound < Exception; end
22 class ChangesetNotFound < Exception; end
23 class InvalidRevisionParam < Exception; end
23 class InvalidRevisionParam < Exception; end
24
24
25 class RepositoriesController < ApplicationController
25 class RepositoriesController < ApplicationController
26 layout 'base'
26 layout 'base'
27 menu_item :repository
27 menu_item :repository
28 before_filter :find_repository, :except => :edit
28 before_filter :find_repository, :except => :edit
29 before_filter :find_project, :only => :edit
29 before_filter :find_project, :only => :edit
30 before_filter :authorize
30 before_filter :authorize
31 accept_key_auth :revisions
31 accept_key_auth :revisions
32
32
33 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
33 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
34
34
35 def edit
35 def edit
36 @repository = @project.repository
36 @repository = @project.repository
37 if !@repository
37 if !@repository
38 @repository = Repository.factory(params[:repository_scm])
38 @repository = Repository.factory(params[:repository_scm])
39 @repository.project = @project if @repository
39 @repository.project = @project if @repository
40 end
40 end
41 if request.post? && @repository
41 if request.post? && @repository
42 @repository.attributes = params[:repository]
42 @repository.attributes = params[:repository]
43 @repository.save
43 @repository.save
44 end
44 end
45 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
45 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
46 end
46 end
47
47
48 def destroy
48 def destroy
49 @repository.destroy
49 @repository.destroy
50 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
50 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
51 end
51 end
52
52
53 def show
53 def show
54 # check if new revisions have been committed in the repository
54 # check if new revisions have been committed in the repository
55 @repository.fetch_changesets if Setting.autofetch_changesets?
55 @repository.fetch_changesets if Setting.autofetch_changesets?
56 # root entries
56 # root entries
57 @entries = @repository.entries('', @rev)
57 @entries = @repository.entries('', @rev)
58 # latest changesets
58 # latest changesets
59 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
59 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
60 show_error_not_found unless @entries || @changesets.any?
60 show_error_not_found unless @entries || @changesets.any?
61 end
61 end
62
62
63 def browse
63 def browse
64 @entries = @repository.entries(@path, @rev)
64 @entries = @repository.entries(@path, @rev)
65 if request.xhr?
65 if request.xhr?
66 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
66 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
67 else
67 else
68 show_error_not_found and return unless @entries
68 show_error_not_found and return unless @entries
69 render :action => 'browse'
69 render :action => 'browse'
70 end
70 end
71 end
71 end
72
72
73 def changes
73 def changes
74 @entry = @repository.entry(@path, @rev)
74 @entry = @repository.entry(@path, @rev)
75 show_error_not_found and return unless @entry
75 show_error_not_found and return unless @entry
76 @changesets = @repository.changesets_for_path(@path)
76 @changesets = @repository.changesets_for_path(@path)
77 end
77 end
78
78
79 def revisions
79 def revisions
80 @changeset_count = @repository.changesets.count
80 @changeset_count = @repository.changesets.count
81 @changeset_pages = Paginator.new self, @changeset_count,
81 @changeset_pages = Paginator.new self, @changeset_count,
82 per_page_option,
82 per_page_option,
83 params['page']
83 params['page']
84 @changesets = @repository.changesets.find(:all,
84 @changesets = @repository.changesets.find(:all,
85 :limit => @changeset_pages.items_per_page,
85 :limit => @changeset_pages.items_per_page,
86 :offset => @changeset_pages.current.offset)
86 :offset => @changeset_pages.current.offset)
87
87
88 respond_to do |format|
88 respond_to do |format|
89 format.html { render :layout => false if request.xhr? }
89 format.html { render :layout => false if request.xhr? }
90 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
90 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
91 end
91 end
92 end
92 end
93
93
94 def entry
94 def entry
95 @entry = @repository.entry(@path, @rev)
95 @entry = @repository.entry(@path, @rev)
96 show_error_not_found and return unless @entry
96 show_error_not_found and return unless @entry
97
97
98 # If the entry is a dir, show the browser
98 # If the entry is a dir, show the browser
99 browse and return if @entry.is_dir?
99 browse and return if @entry.is_dir?
100
100
101 @content = @repository.cat(@path, @rev)
101 @content = @repository.cat(@path, @rev)
102 show_error_not_found and return unless @content
102 show_error_not_found and return unless @content
103 if 'raw' == params[:format] || @content.is_binary_data?
103 if 'raw' == params[:format] || @content.is_binary_data?
104 # Force the download if it's a binary file
104 # Force the download if it's a binary file
105 send_data @content, :filename => @path.split('/').last
105 send_data @content, :filename => @path.split('/').last
106 else
106 else
107 # Prevent empty lines when displaying a file with Windows style eol
107 # Prevent empty lines when displaying a file with Windows style eol
108 @content.gsub!("\r\n", "\n")
108 @content.gsub!("\r\n", "\n")
109 end
109 end
110 end
110 end
111
111
112 def annotate
112 def annotate
113 @annotate = @repository.scm.annotate(@path, @rev)
113 @annotate = @repository.scm.annotate(@path, @rev)
114 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
114 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
115 end
115 end
116
116
117 def revision
117 def revision
118 @changeset = @repository.changesets.find_by_revision(@rev)
118 @changeset = @repository.changesets.find_by_revision(@rev)
119 raise ChangesetNotFound unless @changeset
119 raise ChangesetNotFound unless @changeset
120 @changes_count = @changeset.changes.size
120 @changes_count = @changeset.changes.size
121 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
121 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
122 @changes = @changeset.changes.find(:all,
122 @changes = @changeset.changes.find(:all,
123 :limit => @changes_pages.items_per_page,
123 :limit => @changes_pages.items_per_page,
124 :offset => @changes_pages.current.offset)
124 :offset => @changes_pages.current.offset)
125
125
126 respond_to do |format|
126 respond_to do |format|
127 format.html
127 format.html
128 format.js {render :layout => false}
128 format.js {render :layout => false}
129 end
129 end
130 rescue ChangesetNotFound
130 rescue ChangesetNotFound
131 show_error_not_found
131 show_error_not_found
132 end
132 end
133
133
134 def diff
134 def diff
135 if params[:format] == 'diff'
135 if params[:format] == 'diff'
136 @diff = @repository.diff(@path, @rev, @rev_to)
136 @diff = @repository.diff(@path, @rev, @rev_to)
137 show_error_not_found and return unless @diff
137 show_error_not_found and return unless @diff
138 filename = "changeset_r#{@rev}"
138 filename = "changeset_r#{@rev}"
139 filename << "_r#{@rev_to}" if @rev_to
139 filename << "_r#{@rev_to}" if @rev_to
140 send_data @diff.join, :filename => "#{filename}.diff",
140 send_data @diff.join, :filename => "#{filename}.diff",
141 :type => 'text/x-patch',
141 :type => 'text/x-patch',
142 :disposition => 'attachment'
142 :disposition => 'attachment'
143 else
143 else
144 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
144 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
145 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
145 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
146
146
147 # Save diff type as user preference
147 # Save diff type as user preference
148 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
148 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
149 User.current.pref[:diff_type] = @diff_type
149 User.current.pref[:diff_type] = @diff_type
150 User.current.preference.save
150 User.current.preference.save
151 end
151 end
152
152
153 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
153 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
154 unless read_fragment(@cache_key)
154 unless read_fragment(@cache_key)
155 @diff = @repository.diff(@path, @rev, @rev_to)
155 @diff = @repository.diff(@path, @rev, @rev_to)
156 show_error_not_found unless @diff
156 show_error_not_found unless @diff
157 end
157 end
158 end
158 end
159 end
159 end
160
160
161 def stats
161 def stats
162 end
162 end
163
163
164 def graph
164 def graph
165 data = nil
165 data = nil
166 case params[:graph]
166 case params[:graph]
167 when "commits_per_month"
167 when "commits_per_month"
168 data = graph_commits_per_month(@repository)
168 data = graph_commits_per_month(@repository)
169 when "commits_per_author"
169 when "commits_per_author"
170 data = graph_commits_per_author(@repository)
170 data = graph_commits_per_author(@repository)
171 end
171 end
172 if data
172 if data
173 headers["Content-Type"] = "image/svg+xml"
173 headers["Content-Type"] = "image/svg+xml"
174 send_data(data, :type => "image/svg+xml", :disposition => "inline")
174 send_data(data, :type => "image/svg+xml", :disposition => "inline")
175 else
175 else
176 render_404
176 render_404
177 end
177 end
178 end
178 end
179
179
180 private
180 private
181 def find_project
181 def find_project
182 @project = Project.find(params[:id])
182 @project = Project.find(params[:id])
183 rescue ActiveRecord::RecordNotFound
183 rescue ActiveRecord::RecordNotFound
184 render_404
184 render_404
185 end
185 end
186
186
187 REV_PARAM_RE = %r{^[a-f0-9]*$}
187 REV_PARAM_RE = %r{^[a-f0-9]*$}
188
188
189 def find_repository
189 def find_repository
190 @project = Project.find(params[:id])
190 @project = Project.find(params[:id])
191 @repository = @project.repository
191 @repository = @project.repository
192 render_404 and return false unless @repository
192 render_404 and return false unless @repository
193 @path = params[:path].join('/') unless params[:path].nil?
193 @path = params[:path].join('/') unless params[:path].nil?
194 @path ||= ''
194 @path ||= ''
195 @rev = params[:rev]
195 @rev = params[:rev]
196 @rev_to = params[:rev_to]
196 @rev_to = params[:rev_to]
197 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
197 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
198 rescue ActiveRecord::RecordNotFound
198 rescue ActiveRecord::RecordNotFound
199 render_404
199 render_404
200 rescue InvalidRevisionParam
200 rescue InvalidRevisionParam
201 show_error_not_found
201 show_error_not_found
202 end
202 end
203
203
204 def show_error_not_found
204 def show_error_not_found
205 render_error l(:error_scm_not_found)
205 render_error l(:error_scm_not_found)
206 end
206 end
207
207
208 # Handler for Redmine::Scm::Adapters::CommandFailed exception
208 # Handler for Redmine::Scm::Adapters::CommandFailed exception
209 def show_error_command_failed(exception)
209 def show_error_command_failed(exception)
210 render_error l(:error_scm_command_failed, exception.message)
210 render_error l(:error_scm_command_failed, exception.message)
211 end
211 end
212
212
213 def graph_commits_per_month(repository)
213 def graph_commits_per_month(repository)
214 @date_to = Date.today
214 @date_to = Date.today
215 @date_from = @date_to << 11
215 @date_from = @date_to << 11
216 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
216 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
217 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
217 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
218 commits_by_month = [0] * 12
218 commits_by_month = [0] * 12
219 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
219 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
220
220
221 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
221 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
222 changes_by_month = [0] * 12
222 changes_by_month = [0] * 12
223 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
223 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
224
224
225 fields = []
225 fields = []
226 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
226 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
227 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
227 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
228
228
229 graph = SVG::Graph::Bar.new(
229 graph = SVG::Graph::Bar.new(
230 :height => 300,
230 :height => 300,
231 :width => 500,
231 :width => 800,
232 :fields => fields.reverse,
232 :fields => fields.reverse,
233 :stack => :side,
233 :stack => :side,
234 :scale_integers => true,
234 :scale_integers => true,
235 :step_x_labels => 2,
235 :step_x_labels => 2,
236 :show_data_values => false,
236 :show_data_values => false,
237 :graph_title => l(:label_commits_per_month),
237 :graph_title => l(:label_commits_per_month),
238 :show_graph_title => true
238 :show_graph_title => true
239 )
239 )
240
240
241 graph.add_data(
241 graph.add_data(
242 :data => commits_by_month[0..11].reverse,
242 :data => commits_by_month[0..11].reverse,
243 :title => l(:label_revision_plural)
243 :title => l(:label_revision_plural)
244 )
244 )
245
245
246 graph.add_data(
246 graph.add_data(
247 :data => changes_by_month[0..11].reverse,
247 :data => changes_by_month[0..11].reverse,
248 :title => l(:label_change_plural)
248 :title => l(:label_change_plural)
249 )
249 )
250
250
251 graph.burn
251 graph.burn
252 end
252 end
253
253
254 def graph_commits_per_author(repository)
254 def graph_commits_per_author(repository)
255 commits_by_author = repository.changesets.count(:all, :group => :committer)
255 commits_by_author = repository.changesets.count(:all, :group => :committer)
256 commits_by_author.sort! {|x, y| x.last <=> y.last}
256 commits_by_author.sort! {|x, y| x.last <=> y.last}
257
257
258 changes_by_author = repository.changes.count(:all, :group => :committer)
258 changes_by_author = repository.changes.count(:all, :group => :committer)
259 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
259 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
260
260
261 fields = commits_by_author.collect {|r| r.first}
261 fields = commits_by_author.collect {|r| r.first}
262 commits_data = commits_by_author.collect {|r| r.last}
262 commits_data = commits_by_author.collect {|r| r.last}
263 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
263 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
264
264
265 fields = fields + [""]*(10 - fields.length) if fields.length<10
265 fields = fields + [""]*(10 - fields.length) if fields.length<10
266 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
266 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
267 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
267 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
268
268
269 # Remove email adress in usernames
269 # Remove email adress in usernames
270 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
270 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
271
271
272 graph = SVG::Graph::BarHorizontal.new(
272 graph = SVG::Graph::BarHorizontal.new(
273 :height => 300,
273 :height => 400,
274 :width => 500,
274 :width => 800,
275 :fields => fields,
275 :fields => fields,
276 :stack => :side,
276 :stack => :side,
277 :scale_integers => true,
277 :scale_integers => true,
278 :show_data_values => false,
278 :show_data_values => false,
279 :rotate_y_labels => false,
279 :rotate_y_labels => false,
280 :graph_title => l(:label_commits_per_author),
280 :graph_title => l(:label_commits_per_author),
281 :show_graph_title => true
281 :show_graph_title => true
282 )
282 )
283
283
284 graph.add_data(
284 graph.add_data(
285 :data => commits_data,
285 :data => commits_data,
286 :title => l(:label_revision_plural)
286 :title => l(:label_revision_plural)
287 )
287 )
288
288
289 graph.add_data(
289 graph.add_data(
290 :data => changes_data,
290 :data => changes_data,
291 :title => l(:label_change_plural)
291 :title => l(:label_change_plural)
292 )
292 )
293
293
294 graph.burn
294 graph.burn
295 end
295 end
296
296
297 end
297 end
298
298
299 class Date
299 class Date
300 def months_ago(date = Date.today)
300 def months_ago(date = Date.today)
301 (date.year - self.year)*12 + (date.month - self.month)
301 (date.year - self.year)*12 + (date.month - self.month)
302 end
302 end
303
303
304 def weeks_ago(date = Date.today)
304 def weeks_ago(date = Date.today)
305 (date.year - self.year)*52 + (date.cweek - self.cweek)
305 (date.year - self.year)*52 + (date.cweek - self.cweek)
306 end
306 end
307 end
307 end
308
308
309 class String
309 class String
310 def with_leading_slash
310 def with_leading_slash
311 starts_with?('/') ? self : "/#{self}"
311 starts_with?('/') ? self : "/#{self}"
312 end
312 end
313 end
313 end
@@ -1,13 +1,12
1 <h2><%= l(:label_statistics) %></h2>
1 <h2><%= l(:label_statistics) %></h2>
2
2
3 <table width="100%">
3 <p>
4 <tr><td>
4 <%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
5 <%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
5 </p>
6 </td><td>
6 <p>
7 <%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
7 <%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
8 </td></tr>
8 </p>
9 </table>
9
10 <br />
11 <p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
10 <p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
12
11
13 <% html_title(l(:label_repository), l(:label_statistics)) -%>
12 <% html_title(l(:label_repository), l(:label_statistics)) -%>
General Comments 0
You need to be logged in to leave comments. Login now