annotate app/controllers/.svn/text-base/repositories_controller.rb.svn-base @ 922:ad295b270cd4 live

FIx #446: "non-utf8 paths in repositories blow up repo viewer and reposman" by ensuring the iconv conversion always happens even if source and dest are intended to be the same encoding
author Chris Cannam
date Tue, 13 Mar 2012 16:33:49 +0000
parents 851510f1b535
children
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
Chris@441 39 if !@repository && !params[:repository_scm].blank?
Chris@0 40 @repository = Repository.factory(params[:repository_scm])
Chris@0 41 @repository.project = @project if @repository
Chris@0 42 end
Chris@0 43 if request.post? && @repository
Chris@441 44 p1 = params[:repository]
Chris@441 45 p = {}
Chris@441 46 p_extra = {}
Chris@441 47 p1.each do |k, v|
Chris@441 48 if k =~ /^extra_/
Chris@441 49 p_extra[k] = v
Chris@441 50 else
Chris@441 51 p[k] = v
Chris@441 52 end
Chris@441 53 end
Chris@441 54 @repository.attributes = p
Chris@441 55 @repository.merge_extra_info(p_extra)
Chris@0 56 @repository.save
Chris@0 57 end
Chris@0 58 render(:update) do |page|
Chris@441 59 page.replace_html "tab-content-repository",
Chris@441 60 :partial => 'projects/settings/repository'
Chris@0 61 if @repository && !@project.repository
Chris@441 62 @project.reload # needed to reload association
Chris@0 63 page.replace_html "main-menu", render_main_menu(@project)
Chris@0 64 end
Chris@0 65 end
Chris@0 66 end
Chris@441 67
Chris@0 68 def committers
Chris@0 69 @committers = @repository.committers
Chris@0 70 @users = @project.users
Chris@0 71 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
Chris@0 72 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
Chris@0 73 @users.compact!
Chris@0 74 @users.sort!
Chris@0 75 if request.post? && params[:committers].is_a?(Hash)
Chris@0 76 # Build a hash with repository usernames as keys and corresponding user ids as values
Chris@0 77 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
Chris@0 78 flash[:notice] = l(:notice_successful_update)
Chris@0 79 redirect_to :action => 'committers', :id => @project
Chris@0 80 end
Chris@0 81 end
Chris@245 82
Chris@0 83 def destroy
Chris@0 84 @repository.destroy
Chris@441 85 redirect_to :controller => 'projects',
Chris@441 86 :action => 'settings',
Chris@441 87 :id => @project,
Chris@441 88 :tab => 'repository'
Chris@0 89 end
Chris@245 90
Chris@245 91 def show
Chris@0 92 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
Chris@0 93
Chris@0 94 @entries = @repository.entries(@path, @rev)
Chris@441 95 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 96 if request.xhr?
Chris@0 97 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
Chris@0 98 else
Chris@0 99 (show_error_not_found; return) unless @entries
Chris@0 100 @changesets = @repository.latest_changesets(@path, @rev)
Chris@0 101 @properties = @repository.properties(@path, @rev)
Chris@0 102 render :action => 'show'
Chris@0 103 end
Chris@0 104 end
Chris@0 105
Chris@0 106 alias_method :browse, :show
Chris@245 107
Chris@0 108 def changes
Chris@0 109 @entry = @repository.entry(@path, @rev)
Chris@0 110 (show_error_not_found; return) unless @entry
Chris@0 111 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
Chris@0 112 @properties = @repository.properties(@path, @rev)
Chris@210 113 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 114 end
Chris@245 115
Chris@0 116 def revisions
Chris@0 117 @changeset_count = @repository.changesets.count
Chris@0 118 @changeset_pages = Paginator.new self, @changeset_count,
Chris@245 119 per_page_option,
Chris@245 120 params['page']
Chris@0 121 @changesets = @repository.changesets.find(:all,
Chris@245 122 :limit => @changeset_pages.items_per_page,
Chris@245 123 :offset => @changeset_pages.current.offset,
Chris@245 124 :include => [:user, :repository])
Chris@0 125
Chris@0 126 respond_to do |format|
Chris@0 127 format.html { render :layout => false if request.xhr? }
Chris@0 128 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
Chris@0 129 end
Chris@0 130 end
Chris@245 131
Chris@0 132 def entry
Chris@0 133 @entry = @repository.entry(@path, @rev)
Chris@0 134 (show_error_not_found; return) unless @entry
Chris@0 135
Chris@0 136 # If the entry is a dir, show the browser
Chris@0 137 (show; return) if @entry.is_dir?
Chris@0 138
Chris@0 139 @content = @repository.cat(@path, @rev)
Chris@0 140 (show_error_not_found; return) unless @content
Chris@441 141 if 'raw' == params[:format] ||
Chris@441 142 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
Chris@441 143 ! is_entry_text_data?(@content, @path)
Chris@0 144 # Force the download
Chris@441 145 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
Chris@441 146 send_type = Redmine::MimeType.of(@path)
Chris@441 147 send_opt[:type] = send_type.to_s if send_type
Chris@441 148 send_data @content, send_opt
Chris@0 149 else
Chris@0 150 # Prevent empty lines when displaying a file with Windows style eol
Chris@441 151 # TODO: UTF-16
Chris@441 152 # Is this needs? AttachmentsController reads file simply.
Chris@0 153 @content.gsub!("\r\n", "\n")
Chris@210 154 @changeset = @repository.find_changeset_by_name(@rev)
Chris@441 155 end
Chris@0 156 end
Chris@210 157
Chris@441 158 def is_entry_text_data?(ent, path)
Chris@441 159 # UTF-16 contains "\x00".
Chris@441 160 # It is very strict that file contains less than 30% of ascii symbols
Chris@441 161 # in non Western Europe.
Chris@441 162 return true if Redmine::MimeType.is_type?('text', path)
Chris@441 163 # Ruby 1.8.6 has a bug of integer divisions.
Chris@441 164 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
Chris@441 165 return false if ent.is_binary_data?
Chris@441 166 true
Chris@441 167 end
Chris@441 168 private :is_entry_text_data?
Chris@441 169
Chris@0 170 def annotate
Chris@0 171 @entry = @repository.entry(@path, @rev)
Chris@0 172 (show_error_not_found; return) unless @entry
Chris@245 173
Chris@0 174 @annotate = @repository.scm.annotate(@path, @rev)
Chris@0 175 (render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
Chris@210 176 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 177 end
Chris@210 178
Chris@0 179 def revision
Chris@128 180 raise ChangesetNotFound if @rev.blank?
Chris@0 181 @changeset = @repository.find_changeset_by_name(@rev)
Chris@0 182 raise ChangesetNotFound unless @changeset
Chris@0 183
Chris@0 184 respond_to do |format|
Chris@0 185 format.html
Chris@0 186 format.js {render :layout => false}
Chris@0 187 end
Chris@0 188 rescue ChangesetNotFound
Chris@0 189 show_error_not_found
Chris@0 190 end
Chris@245 191
Chris@0 192 def diff
Chris@0 193 if params[:format] == 'diff'
Chris@0 194 @diff = @repository.diff(@path, @rev, @rev_to)
Chris@0 195 (show_error_not_found; return) unless @diff
Chris@0 196 filename = "changeset_r#{@rev}"
Chris@0 197 filename << "_r#{@rev_to}" if @rev_to
Chris@0 198 send_data @diff.join, :filename => "#{filename}.diff",
Chris@0 199 :type => 'text/x-patch',
Chris@0 200 :disposition => 'attachment'
Chris@0 201 else
Chris@0 202 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
Chris@0 203 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
Chris@441 204
Chris@0 205 # Save diff type as user preference
Chris@0 206 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
Chris@0 207 User.current.pref[:diff_type] = @diff_type
Chris@0 208 User.current.preference.save
Chris@0 209 end
Chris@441 210 @cache_key = "repositories/diff/#{@repository.id}/" +
Chris@507 211 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
Chris@0 212 unless read_fragment(@cache_key)
Chris@0 213 @diff = @repository.diff(@path, @rev, @rev_to)
Chris@0 214 show_error_not_found unless @diff
Chris@0 215 end
Chris@119 216
Chris@119 217 @changeset = @repository.find_changeset_by_name(@rev)
Chris@119 218 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
Chris@119 219 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
Chris@0 220 end
Chris@0 221 end
Chris@119 222
Chris@245 223 def stats
Chris@0 224 end
Chris@245 225
Chris@0 226 def graph
Chris@245 227 data = nil
Chris@0 228 case params[:graph]
Chris@0 229 when "commits_per_month"
Chris@0 230 data = graph_commits_per_month(@repository)
Chris@0 231 when "commits_per_author"
Chris@0 232 data = graph_commits_per_author(@repository)
Chris@0 233 end
Chris@0 234 if data
Chris@0 235 headers["Content-Type"] = "image/svg+xml"
Chris@0 236 send_data(data, :type => "image/svg+xml", :disposition => "inline")
Chris@0 237 else
Chris@0 238 render_404
Chris@0 239 end
Chris@0 240 end
Chris@441 241
Chris@119 242 private
Chris@119 243
Chris@119 244 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
Chris@119 245
Chris@0 246 def find_repository
Chris@0 247 @project = Project.find(params[:id])
Chris@0 248 @repository = @project.repository
Chris@0 249 (render_404; return false) unless @repository
Chris@0 250 @path = params[:path].join('/') unless params[:path].nil?
Chris@0 251 @path ||= ''
Chris@0 252 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
Chris@0 253 @rev_to = params[:rev_to]
Chris@441 254
Chris@245 255 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
Chris@119 256 if @repository.branches.blank?
Chris@119 257 raise InvalidRevisionParam
Chris@119 258 end
Chris@119 259 end
Chris@0 260 rescue ActiveRecord::RecordNotFound
Chris@0 261 render_404
Chris@0 262 rescue InvalidRevisionParam
Chris@0 263 show_error_not_found
Chris@0 264 end
Chris@0 265
Chris@0 266 def show_error_not_found
Chris@128 267 render_error :message => l(:error_scm_not_found), :status => 404
Chris@0 268 end
Chris@441 269
Chris@0 270 # Handler for Redmine::Scm::Adapters::CommandFailed exception
Chris@0 271 def show_error_command_failed(exception)
Chris@0 272 render_error l(:error_scm_command_failed, exception.message)
Chris@0 273 end
Chris@441 274
Chris@0 275 def graph_commits_per_month(repository)
Chris@0 276 @date_to = Date.today
Chris@0 277 @date_from = @date_to << 11
Chris@0 278 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
Chris@441 279 commits_by_day = repository.changesets.count(
Chris@441 280 :all, :group => :commit_date,
Chris@441 281 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
Chris@0 282 commits_by_month = [0] * 12
Chris@0 283 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
Chris@0 284
Chris@441 285 changes_by_day = repository.changes.count(
Chris@441 286 :all, :group => :commit_date,
Chris@441 287 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
Chris@0 288 changes_by_month = [0] * 12
Chris@0 289 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
Chris@441 290
Chris@0 291 fields = []
Chris@0 292 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
Chris@441 293
Chris@0 294 graph = SVG::Graph::Bar.new(
Chris@0 295 :height => 300,
Chris@0 296 :width => 800,
Chris@0 297 :fields => fields.reverse,
Chris@0 298 :stack => :side,
Chris@0 299 :scale_integers => true,
Chris@0 300 :step_x_labels => 2,
Chris@0 301 :show_data_values => false,
Chris@0 302 :graph_title => l(:label_commits_per_month),
Chris@0 303 :show_graph_title => true
Chris@0 304 )
Chris@441 305
Chris@0 306 graph.add_data(
Chris@0 307 :data => commits_by_month[0..11].reverse,
Chris@0 308 :title => l(:label_revision_plural)
Chris@0 309 )
Chris@0 310
Chris@0 311 graph.add_data(
Chris@0 312 :data => changes_by_month[0..11].reverse,
Chris@0 313 :title => l(:label_change_plural)
Chris@0 314 )
Chris@441 315
Chris@0 316 graph.burn
Chris@0 317 end
Chris@0 318
Chris@0 319 def graph_commits_per_author(repository)
Chris@0 320 commits_by_author = repository.changesets.count(:all, :group => :committer)
Chris@0 321 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
Chris@0 322
Chris@0 323 changes_by_author = repository.changes.count(:all, :group => :committer)
Chris@0 324 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
Chris@441 325
Chris@0 326 fields = commits_by_author.collect {|r| r.first}
Chris@0 327 commits_data = commits_by_author.collect {|r| r.last}
Chris@0 328 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
Chris@441 329
Chris@0 330 fields = fields + [""]*(10 - fields.length) if fields.length<10
Chris@0 331 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
Chris@0 332 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
Chris@441 333
Chris@0 334 # Remove email adress in usernames
Chris@0 335 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
Chris@441 336
Chris@0 337 graph = SVG::Graph::BarHorizontal.new(
Chris@0 338 :height => 400,
Chris@0 339 :width => 800,
Chris@0 340 :fields => fields,
Chris@0 341 :stack => :side,
Chris@0 342 :scale_integers => true,
Chris@0 343 :show_data_values => false,
Chris@0 344 :rotate_y_labels => false,
Chris@0 345 :graph_title => l(:label_commits_per_author),
Chris@0 346 :show_graph_title => true
Chris@0 347 )
Chris@0 348 graph.add_data(
Chris@0 349 :data => commits_data,
Chris@0 350 :title => l(:label_revision_plural)
Chris@0 351 )
Chris@0 352 graph.add_data(
Chris@0 353 :data => changes_data,
Chris@0 354 :title => l(:label_change_plural)
Chris@0 355 )
Chris@0 356 graph.burn
Chris@0 357 end
Chris@441 358 end
Chris@0 359
Chris@0 360 class Date
Chris@0 361 def months_ago(date = Date.today)
Chris@0 362 (date.year - self.year)*12 + (date.month - self.month)
Chris@0 363 end
Chris@0 364
Chris@0 365 def weeks_ago(date = Date.today)
Chris@0 366 (date.year - self.year)*52 + (date.cweek - self.cweek)
Chris@0 367 end
Chris@0 368 end
Chris@0 369
Chris@0 370 class String
Chris@0 371 def with_leading_slash
Chris@0 372 starts_with?('/') ? self : "/#{self}"
Chris@0 373 end
Chris@0 374 end