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