File: app/controllers/repositories_controller.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  class: ChangesetNotFound#22
inherits from
  Exception ( Builtin-Module )
  class: InvalidRevisionParam#23
inherits from
  Exception ( Builtin-Module )
  class: RepositoriesController#25
inherits from
  ApplicationController   
has properties
method: new #39
method: create #47
method: edit #57
method: update #60
method: committers #70
method: destroy #85
method: show #90
method: changes #108
method: revisions #116
method: entry #132
method: is_entry_text_data? / 2 #158
method: annotate #170
method: revision #188
method: add_related_issue #197
method: remove_related_issue #226
method: diff #241
method: stats #272
method: graph #275
method: find_repository #293
constant: REV_PARAM_RE #300
method: find_project_repository #302
method: find_changeset #326
method: show_error_not_found #333
method: show_error_command_failed / 1 #338
method: graph_commits_per_month / 1 #342
method: graph_commits_per_author / 1 #386

Class Hierarchy

Code

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