annotate app/controllers/repositories_controller.rb @ 1082:997f6d7738f7 bug_531

In repo controller entry action, show the page for the file even if it's binary (so user still has access to history etc links). This makes it possible to use the entry action as the default when a file is clicked on
author Chris Cannam <chris.cannam@soundsoftware.ac.uk>
date Thu, 22 Nov 2012 18:04:17 +0000
parents 5e80956cc792
children bb32da3bea34
rev   line source
Chris@0 1 # Redmine - project management software
Chris@441 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@441 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@441 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require 'SVG/Graph/Bar'
Chris@0 19 require 'SVG/Graph/BarHorizontal'
Chris@0 20 require 'digest/sha1'
Chris@0 21
Chris@0 22 class ChangesetNotFound < Exception; end
Chris@0 23 class InvalidRevisionParam < Exception; end
Chris@0 24
Chris@0 25 class RepositoriesController < ApplicationController
Chris@0 26 menu_item :repository
Chris@0 27 menu_item :settings, :only => :edit
Chris@0 28 default_search_scope :changesets
Chris@441 29
Chris@0 30 before_filter :find_repository, :except => :edit
Chris@0 31 before_filter :find_project, :only => :edit
Chris@0 32 before_filter :authorize
Chris@507 33 accept_rss_auth :revisions
Chris@441 34
Chris@0 35 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
Chris@441 36
Chris@0 37 def edit
Chris@0 38 @repository = @project.repository
luis@213 39
Chris@0 40 if !@repository
luis@274 41
luis@274 42 params[:repository_scm]='Mercurial'
luis@274 43
Chris@0 44 @repository = Repository.factory(params[:repository_scm])
Chris@0 45 @repository.project = @project if @repository
Chris@0 46 end
Chris@0 47 if request.post? && @repository
Chris@441 48 p1 = params[:repository]
Chris@441 49 p = {}
Chris@441 50 p_extra = {}
Chris@441 51 p1.each do |k, v|
Chris@441 52 if k =~ /^extra_/
Chris@441 53 p_extra[k] = v
Chris@441 54 else
Chris@441 55 p[k] = v
Chris@441 56 end
Chris@441 57 end
Chris@441 58 @repository.attributes = p
Chris@441 59 @repository.merge_extra_info(p_extra)
Chris@0 60 @repository.save
Chris@0 61 end
luis@272 62
Chris@0 63 render(:update) do |page|
Chris@441 64 page.replace_html "tab-content-repository",
Chris@441 65 :partial => 'projects/settings/repository'
Chris@0 66 if @repository && !@project.repository
Chris@441 67 @project.reload # needed to reload association
Chris@0 68 page.replace_html "main-menu", render_main_menu(@project)
Chris@0 69 end
Chris@0 70 end
Chris@0 71 end
Chris@441 72
Chris@0 73 def committers
Chris@0 74 @committers = @repository.committers
Chris@0 75 @users = @project.users
Chris@0 76 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
Chris@0 77 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
Chris@0 78 @users.compact!
Chris@0 79 @users.sort!
Chris@0 80 if request.post? && params[:committers].is_a?(Hash)
Chris@0 81 # Build a hash with repository usernames as keys and corresponding user ids as values
Chris@0 82 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
Chris@0 83 flash[:notice] = l(:notice_successful_update)
Chris@0 84 redirect_to :action => 'committers', :id => @project
Chris@0 85 end
Chris@0 86 end
Chris@245 87
Chris@0 88 def destroy
Chris@0 89 @repository.destroy
Chris@441 90 redirect_to :controller => 'projects',
Chris@441 91 :action => 'settings',
Chris@441 92 :id => @project,
Chris@441 93 :tab => 'repository'
Chris@0 94 end
Chris@245 95
Chris@245 96 def show
Chris@0 97 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
Chris@0 98
Chris@0 99 @entries = @repository.entries(@path, @rev)
Chris@441 100 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 101 if request.xhr?
Chris@0 102 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
Chris@0 103 else
Chris@0 104 (show_error_not_found; return) unless @entries
Chris@0 105 @changesets = @repository.latest_changesets(@path, @rev)
Chris@0 106 @properties = @repository.properties(@path, @rev)
Chris@0 107 render :action => 'show'
Chris@0 108 end
Chris@0 109 end
Chris@0 110
Chris@0 111 alias_method :browse, :show
Chris@245 112
Chris@0 113 def changes
Chris@0 114 @entry = @repository.entry(@path, @rev)
Chris@0 115 (show_error_not_found; return) unless @entry
Chris@0 116 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
Chris@0 117 @properties = @repository.properties(@path, @rev)
Chris@210 118 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 119 end
Chris@245 120
Chris@0 121 def revisions
Chris@0 122 @changeset_count = @repository.changesets.count
Chris@0 123 @changeset_pages = Paginator.new self, @changeset_count,
Chris@245 124 per_page_option,
Chris@245 125 params['page']
Chris@0 126 @changesets = @repository.changesets.find(:all,
Chris@245 127 :limit => @changeset_pages.items_per_page,
Chris@245 128 :offset => @changeset_pages.current.offset,
Chris@909 129 :include => [:user, :repository, :parents])
Chris@0 130
Chris@0 131 respond_to do |format|
Chris@0 132 format.html { render :layout => false if request.xhr? }
Chris@0 133 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
Chris@0 134 end
Chris@0 135 end
Chris@245 136
Chris@0 137 def entry
Chris@0 138 @entry = @repository.entry(@path, @rev)
Chris@0 139 (show_error_not_found; return) unless @entry
Chris@0 140
Chris@0 141 # If the entry is a dir, show the browser
Chris@0 142 (show; return) if @entry.is_dir?
Chris@0 143
Chris@0 144 @content = @repository.cat(@path, @rev)
Chris@0 145 (show_error_not_found; return) unless @content
chris@1082 146 if 'raw' == params[:format]
Chris@0 147 # Force the download
Chris@441 148 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
Chris@441 149 send_type = Redmine::MimeType.of(@path)
Chris@441 150 send_opt[:type] = send_type.to_s if send_type
Chris@441 151 send_data @content, send_opt
Chris@0 152 else
chris@1082 153 @display_raw = ((@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) || !is_entry_text_data?(@content, @path))
Chris@0 154 # Prevent empty lines when displaying a file with Windows style eol
Chris@441 155 # TODO: UTF-16
Chris@441 156 # Is this needs? AttachmentsController reads file simply.
Chris@0 157 @content.gsub!("\r\n", "\n")
Chris@210 158 @changeset = @repository.find_changeset_by_name(@rev)
Chris@441 159 end
Chris@0 160 end
Chris@210 161
Chris@441 162 def is_entry_text_data?(ent, path)
Chris@441 163 # UTF-16 contains "\x00".
Chris@441 164 # It is very strict that file contains less than 30% of ascii symbols
Chris@441 165 # in non Western Europe.
Chris@441 166 return true if Redmine::MimeType.is_type?('text', path)
Chris@441 167 # Ruby 1.8.6 has a bug of integer divisions.
Chris@441 168 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
Chris@441 169 return false if ent.is_binary_data?
Chris@441 170 true
Chris@441 171 end
Chris@441 172 private :is_entry_text_data?
Chris@441 173
Chris@0 174 def annotate
Chris@0 175 @entry = @repository.entry(@path, @rev)
Chris@0 176 (show_error_not_found; return) unless @entry
Chris@245 177
Chris@0 178 @annotate = @repository.scm.annotate(@path, @rev)
Chris@909 179 if @annotate.nil? || @annotate.empty?
Chris@909 180 (render_error l(:error_scm_annotate); return)
Chris@909 181 end
Chris@909 182 ann_buf_size = 0
Chris@909 183 @annotate.lines.each do |buf|
Chris@909 184 ann_buf_size += buf.size
Chris@909 185 end
Chris@909 186 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
Chris@909 187 (render_error l(:error_scm_annotate_big_text_file); return)
Chris@909 188 end
Chris@210 189 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 190 end
Chris@210 191
Chris@0 192 def revision
Chris@128 193 raise ChangesetNotFound if @rev.blank?
Chris@0 194 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 195 raise ChangesetNotFound unless @changeset
Chris@0 196
Chris@0 197 respond_to do |format|
Chris@0 198 format.html
Chris@0 199 format.js {render :layout => false}
Chris@0 200 end
Chris@0 201 rescue ChangesetNotFound
Chris@0 202 show_error_not_found
Chris@0 203 end
Chris@245 204
Chris@0 205 def diff
Chris@0 206 if params[:format] == 'diff'
Chris@0 207 @diff = @repository.diff(@path, @rev, @rev_to)
Chris@0 208 (show_error_not_found; return) unless @diff
Chris@0 209 filename = "changeset_r#{@rev}"
Chris@0 210 filename << "_r#{@rev_to}" if @rev_to
Chris@0 211 send_data @diff.join, :filename => "#{filename}.diff",
Chris@0 212 :type => 'text/x-patch',
Chris@0 213 :disposition => 'attachment'
Chris@0 214 else
Chris@0 215 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
Chris@0 216 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
Chris@441 217
Chris@0 218 # Save diff type as user preference
Chris@0 219 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
Chris@0 220 User.current.pref[:diff_type] = @diff_type
Chris@0 221 User.current.preference.save
Chris@0 222 end
Chris@909 223 @cache_key = "repositories/diff/#{@repository.id}/" +
Chris@507 224 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
Chris@0 225 unless read_fragment(@cache_key)
Chris@0 226 @diff = @repository.diff(@path, @rev, @rev_to)
Chris@0 227 show_error_not_found unless @diff
Chris@0 228 end
Chris@119 229
Chris@119 230 @changeset = @repository.find_changeset_by_name(@rev)
Chris@119 231 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
Chris@119 232 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
Chris@0 233 end
Chris@0 234 end
Chris@119 235
Chris@245 236 def stats
Chris@0 237 end
Chris@245 238
Chris@0 239 def graph
Chris@245 240 data = nil
Chris@0 241 case params[:graph]
Chris@0 242 when "commits_per_month"
Chris@0 243 data = graph_commits_per_month(@repository)
Chris@0 244 when "commits_per_author"
Chris@0 245 data = graph_commits_per_author(@repository)
Chris@0 246 end
Chris@0 247 if data
Chris@0 248 headers["Content-Type"] = "image/svg+xml"
Chris@0 249 send_data(data, :type => "image/svg+xml", :disposition => "inline")
Chris@0 250 else
Chris@0 251 render_404
Chris@0 252 end
Chris@0 253 end
Chris@441 254
Chris@119 255 private
Chris@119 256
Chris@119 257 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
Chris@119 258
Chris@0 259 def find_repository
Chris@0 260 @project = Project.find(params[:id])
Chris@0 261 @repository = @project.repository
Chris@0 262 (render_404; return false) unless @repository
Chris@0 263 @path = params[:path].join('/') unless params[:path].nil?
Chris@0 264 @path ||= ''
Chris@909 265 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
Chris@0 266 @rev_to = params[:rev_to]
Chris@441 267
Chris@245 268 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
Chris@119 269 if @repository.branches.blank?
Chris@119 270 raise InvalidRevisionParam
Chris@119 271 end
Chris@119 272 end
Chris@0 273 rescue ActiveRecord::RecordNotFound
Chris@0 274 render_404
Chris@0 275 rescue InvalidRevisionParam
Chris@0 276 show_error_not_found
Chris@0 277 end
Chris@0 278
Chris@0 279 def show_error_not_found
Chris@128 280 render_error :message => l(:error_scm_not_found), :status => 404
Chris@0 281 end
Chris@441 282
Chris@0 283 # Handler for Redmine::Scm::Adapters::CommandFailed exception
Chris@0 284 def show_error_command_failed(exception)
Chris@0 285 render_error l(:error_scm_command_failed, exception.message)
Chris@0 286 end
Chris@441 287
Chris@0 288 def graph_commits_per_month(repository)
Chris@0 289 @date_to = Date.today
Chris@0 290 @date_from = @date_to << 11
Chris@0 291 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
Chris@441 292 commits_by_day = repository.changesets.count(
Chris@441 293 :all, :group => :commit_date,
Chris@441 294 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
Chris@0 295 commits_by_month = [0] * 12
Chris@0 296 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
Chris@0 297
Chris@441 298 changes_by_day = repository.changes.count(
Chris@441 299 :all, :group => :commit_date,
Chris@441 300 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
Chris@0 301 changes_by_month = [0] * 12
Chris@0 302 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
Chris@441 303
Chris@0 304 fields = []
Chris@0 305 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
Chris@441 306
Chris@0 307 graph = SVG::Graph::Bar.new(
Chris@0 308 :height => 300,
Chris@0 309 :width => 800,
Chris@0 310 :fields => fields.reverse,
Chris@0 311 :stack => :side,
Chris@0 312 :scale_integers => true,
Chris@0 313 :step_x_labels => 2,
Chris@0 314 :show_data_values => false,
Chris@0 315 :graph_title => l(:label_commits_per_month),
Chris@0 316 :show_graph_title => true
Chris@0 317 )
Chris@441 318
Chris@0 319 graph.add_data(
Chris@0 320 :data => commits_by_month[0..11].reverse,
Chris@0 321 :title => l(:label_revision_plural)
Chris@0 322 )
Chris@0 323
Chris@0 324 graph.add_data(
Chris@0 325 :data => changes_by_month[0..11].reverse,
Chris@0 326 :title => l(:label_change_plural)
Chris@0 327 )
Chris@441 328
Chris@0 329 graph.burn
Chris@0 330 end
Chris@0 331
Chris@0 332 def graph_commits_per_author(repository)
Chris@0 333 commits_by_author = repository.changesets.count(:all, :group => :committer)
Chris@0 334 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
Chris@0 335
Chris@0 336 changes_by_author = repository.changes.count(:all, :group => :committer)
Chris@0 337 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
Chris@441 338
Chris@0 339 fields = commits_by_author.collect {|r| r.first}
Chris@0 340 commits_data = commits_by_author.collect {|r| r.last}
Chris@0 341 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
Chris@441 342
Chris@0 343 fields = fields + [""]*(10 - fields.length) if fields.length<10
Chris@0 344 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
Chris@0 345 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
Chris@441 346
Chris@0 347 # Remove email adress in usernames
Chris@0 348 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
Chris@441 349
Chris@0 350 graph = SVG::Graph::BarHorizontal.new(
Chris@0 351 :height => 400,
Chris@0 352 :width => 800,
Chris@0 353 :fields => fields,
Chris@0 354 :stack => :side,
Chris@0 355 :scale_integers => true,
Chris@0 356 :show_data_values => false,
Chris@0 357 :rotate_y_labels => false,
Chris@0 358 :graph_title => l(:label_commits_per_author),
Chris@0 359 :show_graph_title => true
Chris@0 360 )
Chris@0 361 graph.add_data(
Chris@0 362 :data => commits_data,
Chris@0 363 :title => l(:label_revision_plural)
Chris@0 364 )
Chris@0 365 graph.add_data(
Chris@0 366 :data => changes_data,
Chris@0 367 :title => l(:label_change_plural)
Chris@0 368 )
Chris@0 369 graph.burn
Chris@0 370 end
Chris@441 371 end
Chris@0 372
Chris@0 373 class Date
Chris@0 374 def months_ago(date = Date.today)
Chris@0 375 (date.year - self.year)*12 + (date.month - self.month)
Chris@0 376 end
Chris@0 377
Chris@0 378 def weeks_ago(date = Date.today)
Chris@0 379 (date.year - self.year)*52 + (date.cweek - self.cweek)
Chris@0 380 end
Chris@0 381 end
Chris@0 382
Chris@0 383 class String
Chris@0 384 def with_leading_slash
Chris@0 385 starts_with?('/') ? self : "/#{self}"
Chris@0 386 end
Chris@0 387 end