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 14KB

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