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