Make sure cache expires the projects activity feed entries, just like repository...
[gitorious:thomas-mainline.git] / app / controllers / repositories_controller.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2012 Gitorious AS
4 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
5 #   Copyright (C) 2009 Fabio Akita <fabio.akita@gmail.com>
6 #   Copyright (C) 2008 David A. Cuadrado <krawek@gmail.com>
7 #   Copyright (C) 2008 Tor Arne Vestbø <tavestbo@trolltech.com>
8 #   Copyright (C) 2007, 2008 Johan Sørensen <johan@johansorensen.com>
9 #
10 #   This program is free software: you can redistribute it and/or modify
11 #   it under the terms of the GNU Affero General Public License as published by
12 #   the Free Software Foundation, either version 3 of the License, or
13 #   (at your option) any later version.
14 #
15 #   This program is distributed in the hope that it will be useful,
16 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 #   GNU Affero General Public License for more details.
19 #
20 #   You should have received a copy of the GNU Affero General Public License
21 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 #++
23
24 class RepositoriesController < ApplicationController
25   before_filter :login_required,
26     :except => [:index, :show, :writable_by, :config, :search_clones]
27   before_filter :find_repository_owner, :except => [:writable_by, :config]
28   before_filter :unauthorized_repository_owner_and_project, :only => [:writable_by, :config]
29   before_filter :require_owner_adminship, :only => [:new, :create]
30   before_filter :find_and_require_repository_adminship,
31     :only => [:edit, :update, :confirm_delete, :destroy]
32   before_filter :require_user_has_ssh_keys, :only => [:clone, :create_clone]
33   before_filter :only_projects_can_add_new_repositories, :only => [:new, :create]
34   always_skip_session :only => [:config, :writable_by]
35   renders_in_site_specific_context :except => [:writable_by, :config]
36
37   def index
38     if term = params[:filter]
39       @repositories = filter(@project.search_repositories(term))
40     else
41       @repositories = paginate(page_free_redirect_options) do
42         filter_paginated(params[:page], Repository.per_page) do |page|
43           @owner.repositories.regular.paginate(:all, :include => [:user, :events, :project], :page => page)
44         end
45       end
46     end
47
48     return if @repositories.count == 0 && params.key?(:page)
49
50     respond_to do |wants|
51       wants.html
52       wants.xml {render :xml => @repositories.to_xml}
53       wants.json {render :json => to_json(@repositories)}
54     end
55   end
56
57   def show
58     @root = @repository = repository_to_clone
59     @events = paginated_events
60
61     return if @events.count == 0 && params.key?(:page)
62     @atom_auto_discovery_url = repo_owner_path(@repository, :project_repository_path,
63                                   @repository.project, @repository, :format => :atom)
64     response.headers["Refresh"] = "5" unless @repository.ready
65
66     respond_to do |format|
67       format.html
68       format.xml  { render :xml => @repository }
69       format.atom {  }
70     end
71   end
72
73   def paginated_events
74     paginate(page_free_redirect_options) do
75       if !private_repositories_enabled?
76         Rails.cache.fetch("paginated_events_in_repo_#{@repository.id}:#{params[:page] || 1}", :expires_in => 10.minutes) do
77           unfiltered_paginated_events
78         end
79       else
80         filter_paginated(params[:page], Event.per_page) do |page|
81           unfiltered_paginated_events
82         end
83       end
84     end
85   end
86
87   def unfiltered_paginated_events
88     @repository.events.top.paginate(:all, :page => params[:page], :order => "created_at desc")
89   end
90
91   def new
92     @repository = @project.repositories.new
93     @root = Breadcrumb::NewRepository.new(@project)
94     @repository.kind = Repository::KIND_PROJECT_REPO
95     @repository.owner = @project.owner
96     if @project.repositories.mainlines.count == 0
97       @repository.name = @project.slug
98     end
99   end
100
101   def create
102     @repository = @project.repositories.new(params[:repository])
103     @root = Breadcrumb::NewRepository.new(@project)
104     @repository.kind = Repository::KIND_PROJECT_REPO
105     @repository.owner = @project.owner
106     @repository.user = current_user
107     @repository.merge_requests_enabled = params[:repository][:merge_requests_enabled]
108
109     if @repository.save
110       @repository.make_private if GitoriousConfig["enable_private_repositories"] && params[:private_repository]
111       flash[:success] = I18n.t("repositories_controller.create_success")
112       redirect_to [@repository.project_or_owner, @repository]
113     else
114       render :action => "new"
115     end
116   end
117
118   undef_method :clone
119
120   def clone
121     @repository_to_clone = repository_to_clone
122     @root = Breadcrumb::CloneRepository.new(@repository_to_clone)
123     unless @repository_to_clone.has_commits?
124       flash[:error] = I18n.t "repositories_controller.new_clone_error"
125       redirect_to [@owner, @repository_to_clone]
126       return
127     end
128     @repository = Repository.new_by_cloning(@repository_to_clone, current_user.login)
129   end
130
131   def create_clone
132     @repository_to_clone = repository_to_clone
133     @root = Breadcrumb::CloneRepository.new(@repository_to_clone)
134     unless @repository_to_clone.has_commits?
135       respond_to do |format|
136         format.html do
137           flash[:error] = I18n.t "repositories_controller.create_clone_error"
138           redirect_to [@owner, @repository_to_clone]
139         end
140         format.xml do
141           render :text => I18n.t("repositories_controller.create_clone_error"),
142             :location => [@owner, @repository_to_clone], :status => :unprocessable_entity
143         end
144       end
145       return
146     end
147
148     @repository = Repository.new_by_cloning(@repository_to_clone)
149     @repository.name = params[:repository][:name]
150     @repository.user = current_user
151     case params[:repository][:owner_type]
152     when "User"
153       @repository.owner = current_user
154       @repository.kind = Repository::KIND_USER_REPO
155     when "Group"
156       @repository.owner = current_user.groups.find(params[:repository][:owner_id])
157       @repository.kind = Repository::KIND_TEAM_REPO
158     end
159
160     respond_to do |format|
161       if @repository.save
162         @owner.create_event(Action::CLONE_REPOSITORY, @repository, current_user, @repository_to_clone.id)
163
164         location = repo_owner_path(@repository, :project_repository_path, @owner, @repository)
165         format.html { redirect_to location }
166         format.xml  { render :xml => @repository, :status => :created, :location => location }
167       else
168         format.html { render :action => "clone" }
169         format.xml  { render :xml => @repository.errors, :status => :unprocessable_entity }
170       end
171     end
172   end
173
174   def edit
175     @root = Breadcrumb::EditRepository.new(@repository)
176     @groups = current_user.groups
177     @heads = @repository.git.heads
178   end
179
180   def search_clones
181     @repository = repository_to_clone
182     @repositories = filter(@repository.search_clones(params[:filter]))
183     render :json => to_json(@repositories)
184   end
185
186   def update
187     @root = Breadcrumb::EditRepository.new(@repository)
188     @groups = current_user.groups
189     @heads = @repository.git.heads
190
191     Repository.transaction do
192       unless params[:repository][:owner_id].blank?
193         @repository.change_owner_to!(current_user.groups.find(params[:repository][:owner_id]))
194       end
195
196       @repository.head = params[:repository][:head]
197
198       @repository.log_changes_with_user(current_user) do
199         @repository.replace_value(:name, params[:repository][:name])
200         @repository.replace_value(:description, params[:repository][:description], true)
201       end
202       @repository.deny_force_pushing = params[:repository][:deny_force_pushing]
203       @repository.notify_committers_on_new_merge_request = params[:repository][:notify_committers_on_new_merge_request]
204       @repository.merge_requests_enabled = params[:repository][:merge_requests_enabled]
205       @repository.save!
206       flash[:success] = "Repository updated"
207       redirect_to [@repository.project_or_owner, @repository]
208     end
209   rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound
210     render :action => "edit"
211   end
212
213   # Used internally to check write permissions by gitorious
214   def writable_by
215     @repository = @owner.cloneable_repositories.find_by_name_in_project!(params[:id], @containing_project)
216     user = User.find_by_login(params[:username])
217
218     if user && result = /^refs\/merge-requests\/(\d+)$/.match(params[:git_path].to_s)
219       # git_path is a merge request
220       begin
221         if merge_request = @repository.merge_requests.find_by_sequence_number!(result[1]) and (merge_request.user == user)
222           render :text => "true" and return
223         end
224       rescue ActiveRecord::RecordNotFound # No such merge request
225       end
226     elsif user && can_push?(user, @repository)
227       render :text => "true" and return
228     end
229     render :text => 'false' and return
230   end
231
232   def config
233     @repository = @owner.cloneable_repositories.find_by_name_in_project!(params[:id],
234       @containing_project)
235     authorize_configuration_access(@repository)
236     config_data = "real_path:#{@repository.real_gitdir}\n"
237     config_data << "force_pushing_denied:"
238     config_data << (@repository.deny_force_pushing? ? 'true' : 'false')
239     headers["Cache-Control"] = "public, max-age=600"
240
241     render :text => config_data, :content_type => "text/x-yaml"
242   end
243
244   def confirm_delete
245     @repository = repository_to_clone
246     unless can_delete?(current_user, @repository)
247       flash[:error] = I18n.t "repositories_controller.adminship_error"
248       redirect_to(@owner) and return
249     end
250   end
251
252   def destroy
253     @repository = repository_to_clone
254     if can_delete?(current_user, @repository)
255       repo_name = @repository.name
256       flash[:notice] = I18n.t "repositories_controller.destroy_notice"
257       @repository.destroy
258       @repository.project.create_event(Action::DELETE_REPOSITORY, @owner,
259                                         current_user, repo_name)
260     else
261       flash[:error] = I18n.t "repositories_controller.destroy_error"
262     end
263     redirect_to @owner
264   end
265
266   private
267   def require_owner_adminship
268     unless admin?(current_user, @owner)
269       respond_denied_and_redirect_to(@owner)
270       return
271     end
272   end
273
274   def find_and_require_repository_adminship
275     @repository = @owner.repositories.find_by_name_in_project!(params[:id],
276                                                                @containing_project)
277     unless admin?(current_user, authorize_access_to(@repository))
278       respond_denied_and_redirect_to(repo_owner_path(@repository,
279                                                      :project_repository_path, @owner, @repository))
280       return
281     end
282   end
283
284   def respond_denied_and_redirect_to(target)
285     respond_to do |format|
286       format.html {
287         flash[:error] = I18n.t "repositories_controller.adminship_error"
288         redirect_to(target)
289       }
290       format.xml  {
291         render :text => I18n.t( "repositories_controller.adminship_error"),
292         :status => :forbidden
293       }
294     end
295   end
296
297   def to_json(repositories)
298     repositories.map { |repo|
299       {
300         :name => repo.name,
301         :description => repo.description,
302         :uri => url_for(project_repository_path(@project, repo)),
303         :img => repo.owner.avatar? ?
304         repo.owner.avatar.url(:thumb) :
305         "/images/default_face.gif",
306         :owner => repo.owner.title,
307         :owner_type => repo.owner_type.downcase,
308         :owner_uri => url_for(repo.owner)
309       }
310     }.to_json
311   end
312
313   def only_projects_can_add_new_repositories
314     if !@owner.is_a?(Project)
315       respond_to do |format|
316         format.html {
317           flash[:error] = I18n.t("repositories_controller.only_projects_create_new_error")
318           redirect_to(@owner)
319         }
320         format.xml  {
321           render :text => I18n.t( "repositories_controller.only_projects_create_new_error"),
322           :status => :forbidden
323         }
324       end
325       return
326     end
327   end
328
329   def unauthorized_repository_owner_and_project
330     if params[:user_id]
331       @owner = User.find_by_login!(params[:user_id])
332       @containing_project = Project.find_by_slug!(params[:project_id]) if params[:project_id]
333     elsif params[:group_id]
334       @owner = Group.find_by_name!(params[:group_id])
335       @containing_project = Project.find_by_slug!(params[:project_id]) if params[:project_id]
336     elsif params[:project_id]
337       @owner = Project.find_by_slug!(params[:project_id])
338       @project = @owner
339     else
340       raise ActiveRecord::RecordNotFound
341     end
342   end
343
344   def authorize_configuration_access(repository)
345     return true if !GitoriousConfig["enable_private_repositories"]
346     if !can_read?(User.find_by_login(params[:username]), repository)
347       raise Gitorious::Authorization::UnauthorizedError.new(request.request_uri)
348     end
349   end
350
351   def repository_to_clone
352     repo = @owner.repositories.find_by_name_in_project!(params[:id], @containing_project)
353     authorize_access_to(repo)
354   end
355 end