PageRenderTime 80ms CodeModel.GetById 40ms app.highlight 34ms RepoModel.GetById 1ms app.codeStats 0ms

/app/controllers/repositories_controller.rb

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