You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

repositories_controller.rb 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. # redMine - project management software
  2. # Copyright (C) 2006-2007 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. require 'SVG/Graph/Bar'
  18. require 'SVG/Graph/BarHorizontal'
  19. require 'digest/sha1'
  20. class ChangesetNotFound < Exception; end
  21. class InvalidRevisionParam < Exception; end
  22. class RepositoriesController < ApplicationController
  23. layout 'base'
  24. menu_item :repository
  25. before_filter :find_repository, :except => :edit
  26. before_filter :find_project, :only => :edit
  27. before_filter :authorize
  28. accept_key_auth :revisions
  29. def edit
  30. @repository = @project.repository
  31. if !@repository
  32. @repository = Repository.factory(params[:repository_scm])
  33. @repository.project = @project
  34. end
  35. if request.post?
  36. @repository.attributes = params[:repository]
  37. @repository.save
  38. end
  39. render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
  40. end
  41. def destroy
  42. @repository.destroy
  43. redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
  44. end
  45. def show
  46. # check if new revisions have been committed in the repository
  47. @repository.fetch_changesets if Setting.autofetch_changesets?
  48. # root entries
  49. @entries = @repository.entries('', @rev)
  50. # latest changesets
  51. @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
  52. show_error_not_found unless @entries || @changesets.any?
  53. rescue Redmine::Scm::Adapters::CommandFailed => e
  54. show_error_command_failed(e.message)
  55. end
  56. def browse
  57. @entries = @repository.entries(@path, @rev)
  58. if request.xhr?
  59. @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
  60. else
  61. show_error_not_found and return unless @entries
  62. render :action => 'browse'
  63. end
  64. rescue Redmine::Scm::Adapters::CommandFailed => e
  65. show_error_command_failed(e.message)
  66. end
  67. def changes
  68. @entry = @repository.scm.entry(@path, @rev)
  69. show_error_not_found and return unless @entry
  70. @changesets = @repository.changesets_for_path(@path)
  71. rescue Redmine::Scm::Adapters::CommandFailed => e
  72. show_error_command_failed(e.message)
  73. end
  74. def revisions
  75. @changeset_count = @repository.changesets.count
  76. @changeset_pages = Paginator.new self, @changeset_count,
  77. per_page_option,
  78. params['page']
  79. @changesets = @repository.changesets.find(:all,
  80. :limit => @changeset_pages.items_per_page,
  81. :offset => @changeset_pages.current.offset)
  82. respond_to do |format|
  83. format.html { render :layout => false if request.xhr? }
  84. format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
  85. end
  86. end
  87. def entry
  88. @entry = @repository.scm.entry(@path, @rev)
  89. show_error_not_found and return unless @entry
  90. # If the entry is a dir, show the browser
  91. browse and return if @entry.is_dir?
  92. @content = @repository.scm.cat(@path, @rev)
  93. show_error_not_found and return unless @content
  94. if 'raw' == params[:format] || @content.is_binary_data?
  95. # Force the download if it's a binary file
  96. send_data @content, :filename => @path.split('/').last
  97. else
  98. # Prevent empty lines when displaying a file with Windows style eol
  99. @content.gsub!("\r\n", "\n")
  100. end
  101. rescue Redmine::Scm::Adapters::CommandFailed => e
  102. show_error_command_failed(e.message)
  103. end
  104. def annotate
  105. @annotate = @repository.scm.annotate(@path, @rev)
  106. render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
  107. rescue Redmine::Scm::Adapters::CommandFailed => e
  108. show_error_command_failed(e.message)
  109. end
  110. def revision
  111. @changeset = @repository.changesets.find_by_revision(@rev)
  112. raise ChangesetNotFound unless @changeset
  113. @changes_count = @changeset.changes.size
  114. @changes_pages = Paginator.new self, @changes_count, 150, params['page']
  115. @changes = @changeset.changes.find(:all,
  116. :limit => @changes_pages.items_per_page,
  117. :offset => @changes_pages.current.offset)
  118. respond_to do |format|
  119. format.html
  120. format.js {render :layout => false}
  121. end
  122. rescue ChangesetNotFound
  123. show_error_not_found
  124. rescue Redmine::Scm::Adapters::CommandFailed => e
  125. show_error_command_failed(e.message)
  126. end
  127. def diff
  128. @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
  129. @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
  130. # Save diff type as user preference
  131. if User.current.logged? && @diff_type != User.current.pref[:diff_type]
  132. User.current.pref[:diff_type] = @diff_type
  133. User.current.preference.save
  134. end
  135. @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
  136. unless read_fragment(@cache_key)
  137. @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
  138. show_error_not_found unless @diff
  139. end
  140. rescue Redmine::Scm::Adapters::CommandFailed => e
  141. show_error_command_failed(e.message)
  142. end
  143. def stats
  144. end
  145. def graph
  146. data = nil
  147. case params[:graph]
  148. when "commits_per_month"
  149. data = graph_commits_per_month(@repository)
  150. when "commits_per_author"
  151. data = graph_commits_per_author(@repository)
  152. end
  153. if data
  154. headers["Content-Type"] = "image/svg+xml"
  155. send_data(data, :type => "image/svg+xml", :disposition => "inline")
  156. else
  157. render_404
  158. end
  159. end
  160. private
  161. def find_project
  162. @project = Project.find(params[:id])
  163. rescue ActiveRecord::RecordNotFound
  164. render_404
  165. end
  166. REV_PARAM_RE = %r{^[a-f0-9]*$}
  167. def find_repository
  168. @project = Project.find(params[:id])
  169. @repository = @project.repository
  170. render_404 and return false unless @repository
  171. @path = params[:path].join('/') unless params[:path].nil?
  172. @path ||= ''
  173. @rev = params[:rev]
  174. @rev_to = params[:rev_to]
  175. raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
  176. rescue ActiveRecord::RecordNotFound
  177. render_404
  178. rescue InvalidRevisionParam
  179. show_error_not_found
  180. end
  181. def show_error_not_found
  182. render_error l(:error_scm_not_found)
  183. end
  184. def show_error_command_failed(msg)
  185. render_error l(:error_scm_command_failed, msg)
  186. end
  187. def graph_commits_per_month(repository)
  188. @date_to = Date.today
  189. @date_from = @date_to << 11
  190. @date_from = Date.civil(@date_from.year, @date_from.month, 1)
  191. commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
  192. commits_by_month = [0] * 12
  193. commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
  194. changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
  195. changes_by_month = [0] * 12
  196. changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
  197. fields = []
  198. month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
  199. 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
  200. graph = SVG::Graph::Bar.new(
  201. :height => 300,
  202. :width => 500,
  203. :fields => fields.reverse,
  204. :stack => :side,
  205. :scale_integers => true,
  206. :step_x_labels => 2,
  207. :show_data_values => false,
  208. :graph_title => l(:label_commits_per_month),
  209. :show_graph_title => true
  210. )
  211. graph.add_data(
  212. :data => commits_by_month[0..11].reverse,
  213. :title => l(:label_revision_plural)
  214. )
  215. graph.add_data(
  216. :data => changes_by_month[0..11].reverse,
  217. :title => l(:label_change_plural)
  218. )
  219. graph.burn
  220. end
  221. def graph_commits_per_author(repository)
  222. commits_by_author = repository.changesets.count(:all, :group => :committer)
  223. commits_by_author.sort! {|x, y| x.last <=> y.last}
  224. changes_by_author = repository.changes.count(:all, :group => :committer)
  225. h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
  226. fields = commits_by_author.collect {|r| r.first}
  227. commits_data = commits_by_author.collect {|r| r.last}
  228. changes_data = commits_by_author.collect {|r| h[r.first] || 0}
  229. fields = fields + [""]*(10 - fields.length) if fields.length<10
  230. commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
  231. changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
  232. # Remove email adress in usernames
  233. fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
  234. graph = SVG::Graph::BarHorizontal.new(
  235. :height => 300,
  236. :width => 500,
  237. :fields => fields,
  238. :stack => :side,
  239. :scale_integers => true,
  240. :show_data_values => false,
  241. :rotate_y_labels => false,
  242. :graph_title => l(:label_commits_per_author),
  243. :show_graph_title => true
  244. )
  245. graph.add_data(
  246. :data => commits_data,
  247. :title => l(:label_revision_plural)
  248. )
  249. graph.add_data(
  250. :data => changes_data,
  251. :title => l(:label_change_plural)
  252. )
  253. graph.burn
  254. end
  255. end
  256. class Date
  257. def months_ago(date = Date.today)
  258. (date.year - self.year)*12 + (date.month - self.month)
  259. end
  260. def weeks_ago(date = Date.today)
  261. (date.year - self.year)*52 + (date.cweek - self.cweek)
  262. end
  263. end
  264. class String
  265. def with_leading_slash
  266. starts_with?('/') ? self : "/#{self}"
  267. end
  268. end