OSDN Git Service

Merge branch 'improve/repo_head_update'
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Mon, 16 Dec 2013 19:30:24 +0000 (21:30 +0200)
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Mon, 16 Dec 2013 19:30:24 +0000 (21:30 +0200)
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Conflicts:
app/models/project.rb

24 files changed:
app/controllers/dashboard_controller.rb
app/controllers/projects/issues_controller.rb
app/controllers/projects/merge_requests_controller.rb
app/controllers/projects/notes_controller.rb
app/controllers/projects_controller.rb
app/helpers/search_helper.rb
app/models/ability.rb
app/models/issue.rb
app/models/merge_request.rb
app/models/note.rb
app/models/project.rb
app/views/dashboard/projects.html.haml
app/views/projects/_home_panel.html.haml
app/views/projects/edit.html.haml
config/routes.rb
db/migrate/20131129154016_add_archived_to_projects.rb [new file with mode: 0644]
db/schema.rb
features/dashboard/archived_projects.feature [new file with mode: 0644]
features/project/archived_projects.feature [new file with mode: 0644]
features/steps/dashboard/dashboard_with_archived_projects.rb [new file with mode: 0644]
features/steps/project/project_archived.rb [new file with mode: 0644]
features/steps/shared/project.rb
spec/models/project_spec.rb
spec/requests/api/internal_spec.rb

index 045e580..aaab4b4 100644 (file)
@@ -73,6 +73,6 @@ class DashboardController < ApplicationController
   protected
 
   def load_projects
-    @projects = current_user.authorized_projects.sorted_by_activity
+    @projects = current_user.authorized_projects.sorted_by_activity.non_archived
   end
 end
index 5dcdba5..e7b4c83 100644 (file)
@@ -74,6 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController
 
   def update
     @issue.update_attributes(params[:issue].merge(author_id_of_changes: current_user.id))
+    @issue.reset_events_cache
 
     respond_to do |format|
       format.js
index 7d7c110..6d39673 100644 (file)
@@ -97,6 +97,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     if @merge_request.update_attributes(params[:merge_request].merge(author_id_of_changes: current_user.id))
       @merge_request.reload_code
       @merge_request.mark_as_unchecked
+      @merge_request.reset_events_cache
       redirect_to [@merge_request.target_project, @merge_request], notice: 'Merge request was successfully updated.'
     else
       render "edit"
index 2738a99..5ff5c5b 100644 (file)
@@ -39,6 +39,7 @@ class Projects::NotesController < Projects::ApplicationController
     @note = @project.notes.find(params[:id])
     return access_denied! unless can?(current_user, :admin_note, @note)
     @note.destroy
+    @note.reset_events_cache
 
     respond_to do |format|
       format.js { render nothing: true }
@@ -50,6 +51,7 @@ class Projects::NotesController < Projects::ApplicationController
     return access_denied! unless can?(current_user, :admin_note, @note)
 
     @note.update_attributes(params[:note])
+    @note.reset_events_cache
 
     respond_to do |format|
       format.js do
index 1835671..e1c55e7 100644 (file)
@@ -5,7 +5,7 @@ class ProjectsController < ApplicationController
 
   # Authorize
   before_filter :authorize_read_project!, except: [:index, :new, :create]
-  before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer]
+  before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive]
   before_filter :require_non_empty_project, only: [:blob, :tree, :graph]
 
   layout 'navless', only: [:new, :create, :fork]
@@ -116,6 +116,24 @@ class ProjectsController < ApplicationController
     end
   end
 
+  def archive
+    return access_denied! unless can?(current_user, :archive_project, project)
+    project.archive!
+
+    respond_to do |format|
+      format.html { redirect_to @project }
+    end
+  end
+
+  def unarchive
+    return access_denied! unless can?(current_user, :archive_project, project)
+    project.unarchive!
+
+    respond_to do |format|
+      format.html { redirect_to @project }
+    end
+  end
+
   private
 
   def set_title
index 109acfd..f24156e 100644 (file)
@@ -73,14 +73,14 @@ module SearchHelper
 
   # Autocomplete results for the current user's projects
   def projects_autocomplete
-    current_user.authorized_projects.map do |p|
+    current_user.authorized_projects.non_archived.map do |p|
       { label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) }
     end
   end
 
   # Autocomplete results for the current user's projects
   def public_projects_autocomplete
-    Project.public_or_internal_only(current_user).map do |p|
+    Project.public_or_internal_only(current_user).non_archived.map do |p|
       { label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) }
     end
   end
index 6df56ee..cf92514 100644 (file)
@@ -59,31 +59,35 @@ class Ability
 
       # Rules based on role in project
       if team.masters.include?(user)
-        rules << project_master_rules
+        rules += project_master_rules
 
       elsif team.developers.include?(user)
-        rules << project_dev_rules
+        rules += project_dev_rules
 
       elsif team.reporters.include?(user)
-        rules << project_report_rules
+        rules += project_report_rules
 
       elsif team.guests.include?(user)
-        rules << project_guest_rules
+        rules += project_guest_rules
       end
 
       if project.public? || project.internal?
-        rules << public_project_rules
+        rules += public_project_rules
       end
 
       if project.owner == user || user.admin?
-        rules << project_admin_rules
+        rules += project_admin_rules
       end
 
       if project.group && project.group.has_owner?(user)
-        rules << project_admin_rules
+        rules += project_admin_rules
       end
 
-      rules.flatten
+      if project.archived?
+        rules -= project_archived_rules
+      end
+
+      rules
     end
 
     def public_project_rules
@@ -125,6 +129,16 @@ class Ability
       ]
     end
 
+    def project_archived_rules
+      [
+        :write_merge_request,
+        :push_code,
+        :push_code_to_protected_branches,
+        :modify_merge_request,
+        :admin_merge_request
+      ]
+    end
+
     def project_master_rules
       project_dev_rules + [
         :push_code_to_protected_branches,
@@ -147,7 +161,8 @@ class Ability
         :change_namespace,
         :change_visibility_level,
         :rename_project,
-        :remove_project
+        :remove_project,
+        :archive_project
       ]
     end
 
@@ -160,7 +175,7 @@ class Ability
 
       # Only group owner and administrators can manage group
       if group.has_owner?(user) || user.admin?
-        rules << [
+        rules += [
           :manage_group,
           :manage_namespace
         ]
@@ -174,7 +189,7 @@ class Ability
 
       # Only namespace owner and administrators can manage it
       if namespace.owner == user || user.admin?
-        rules << [
+        rules += [
           :manage_namespace
         ]
       end
index d350b23..b3609cf 100644 (file)
@@ -64,4 +64,18 @@ class Issue < ActiveRecord::Base
   def gfm_reference
     "issue ##{iid}"
   end
+
+  # Reset issue events cache
+  #
+  # Since we do cache @event we need to reset cache in special cases:
+  # * when an issue is updated
+  # Events cache stored like  events/23-20130109142513.
+  # The cache key includes updated_at timestamp.
+  # Thus it will automatically generate a new fragment
+  # when the event is updated because the key changes.
+  def reset_events_cache
+    Event.where(target_id: self.id, target_type: 'Issue').
+      order('id DESC').limit(100).
+      update_all(updated_at: Time.now)
+  end
 end
index e862f35..e59aee8 100644 (file)
@@ -305,6 +305,20 @@ class MergeRequest < ActiveRecord::Base
     self.target_project.repository.branch_names.include?(self.target_branch)
   end
 
+  # Reset merge request events cache
+  #
+  # Since we do cache @event we need to reset cache in special cases:
+  # * when a merge request is updated
+  # Events cache stored like  events/23-20130109142513.
+  # The cache key includes updated_at timestamp.
+  # Thus it will automatically generate a new fragment
+  # when the event is updated because the key changes.
+  def reset_events_cache
+    Event.where(target_id: self.id, target_type: 'MergeRequest').
+        order('id DESC').limit(100).
+        update_all(updated_at: Time.now)
+  end
+
   private
 
   def dump_commits(commits)
index 8284da8..b23f7df 100644 (file)
@@ -239,4 +239,19 @@ class Note < ActiveRecord::Base
   def noteable_type=(sType)
     super(sType.to_s.classify.constantize.base_class.to_s)
   end
+
+  # Reset notes events cache
+  #
+  # Since we do cache @event we need to reset cache in special cases:
+  # * when a note is updated
+  # * when a note is removed
+  # Events cache stored like  events/23-20130109142513.
+  # The cache key includes updated_at timestamp.
+  # Thus it will automatically generate a new fragment
+  # when the event is updated because the key changes.
+  def reset_events_cache
+    Event.where(target_id: self.id, target_type: 'Note').
+      order('id DESC').limit(100).
+      update_all(updated_at: Time.now)
+  end
 end
index 5f0303d..1bfc27d 100644 (file)
@@ -116,6 +116,8 @@ class Project < ActiveRecord::Base
   scope :public_only, -> { where(visibility_level: PUBLIC) }
   scope :public_or_internal_only, ->(user) { where("visibility_level IN (:levels)", levels: user ? [ INTERNAL, PUBLIC ] : [ PUBLIC ]) }
 
+  scope :non_archived, -> { where(archived: false) }
+
   enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab
 
   class << self
@@ -132,7 +134,7 @@ class Project < ActiveRecord::Base
     end
 
     def search query
-      joins(:namespace).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%")
+      joins(:namespace).where("projects.archived = ?", false).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%")
     end
 
     def find_with_namespace(id)
@@ -473,6 +475,14 @@ class Project < ActiveRecord::Base
     visibility_level
   end
 
+  def archive!
+    update_attribute(:archived, true)
+  end
+
+  def unarchive!
+    update_attribute(:archived, false)
+  end
+
   def change_head(branch)
     gitlab_shell.update_repository_head(self.path_with_namespace, branch)
     reload_default_branch
index b42bbf5..23d7872 100644 (file)
                 = link_to project.forked_from_project.name_with_namespace, project_path(project.forked_from_project)
           .project-info
             .pull-right
+              - if project.archived?
+                %span.label
+                  %i.icon-book
+                  Archived
               - project.labels.each do |label|
                 %span.label.label-info
                   %i.icon-tag
index 19c150b..ae5deb0 100644 (file)
@@ -7,6 +7,10 @@
         %span.visibility-level-label
           = visibility_level_icon(@project.visibility_level)
           = visibility_level_label(@project.visibility_level)
+        - if @project.archived?
+          %span.visibility-level-label
+            %i.icon-book
+            Archived
 
     .span7
       - unless empty_repo
index 57936cf..c56919e 100644 (file)
         %i.icon-chevron-down
 
     .js-toggle-visibility-container.hide
+      - if can? current_user, :archive_project, @project
+        .ui-box.ui-box-danger
+          .title
+            - if @project.archived?
+              Unarchive project
+            - else
+              Archive project
+          .ui-box-body
+            - if @project.archived?
+              %p
+                Unarchiving the project will mark its repository as active.
+                %br
+                The project can be committed to.
+                %br
+                %strong Once active this project shows up in the search and on the dashboard.
+              = link_to 'Unarchive', unarchive_project_path(@project), confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be comitted to again.", method: :post, class: "btn btn-remove"
+            - else
+              %p
+                Archiving the project will mark its repository as read-only.
+                %br
+                It is hidden from the dashboard and doesn't show up in searches.
+                %br
+                %strong Archived projects cannot be committed to!
+              = link_to 'Archive', archive_project_path(@project), confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to.", method: :post, class: "btn btn-remove"
+      - else
+        %p.nothing_here_message Only the project owner can archive a project
+
       - if can?(current_user, :change_namespace, @project)
         .ui-box.ui-box-danger
           .title Transfer project
index 188d209..8322d6a 100644 (file)
@@ -170,6 +170,8 @@ Gitlab::Application.routes.draw do
     member do
       put :transfer
       post :fork
+      post :archive
+      post :unarchive
       get :autocomplete_sources
     end
 
diff --git a/db/migrate/20131129154016_add_archived_to_projects.rb b/db/migrate/20131129154016_add_archived_to_projects.rb
new file mode 100644 (file)
index 0000000..917e690
--- /dev/null
@@ -0,0 +1,5 @@
+class AddArchivedToProjects < ActiveRecord::Migration
+  def change
+    add_column :projects, :archived, :boolean, default: false, null: false
+  end
+end
index e7b3bf0..77d2459 100644 (file)
@@ -192,6 +192,7 @@ ActiveRecord::Schema.define(version: 20131214224427) do
     t.boolean  "imported",               default: false,    null: false
     t.string   "import_url"
     t.integer  "visibility_level",       default: 0,        null: false
+    t.boolean  "archived",               default: false,    null: false
   end
 
   add_index "projects", ["creator_id"], name: "index_projects_on_owner_id", using: :btree
diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature
new file mode 100644 (file)
index 0000000..399c9b5
--- /dev/null
@@ -0,0 +1,16 @@
+Feature: Dashboard with archived projects
+  Background:
+    Given I sign in as a user
+    And I own project "Shop"
+    And I own project "Forum"
+    And project "Forum" is archived
+    And I visit dashboard page
+
+  Scenario: I should see non-archived projects on dashboard
+    Then I should see "Shop" project link
+    And I should not see "Forum" project link
+
+  Scenario: I should see all projects on projects page
+    And I visit dashboard projects page
+    Then I should see "Shop" project link
+    And I should see "Forum" project link
diff --git a/features/project/archived_projects.feature b/features/project/archived_projects.feature
new file mode 100644 (file)
index 0000000..9aac293
--- /dev/null
@@ -0,0 +1,39 @@
+Feature: Project Archived
+  Background:
+    Given I sign in as a user
+    And I own project "Shop"
+    And I own project "Forum"
+
+  Scenario: I should not see archived on project page of not-archive project
+    And project "Forum" is archived
+    And I visit project "Shop" page
+    Then I should not see "Archived"
+
+  Scenario: I should see archived on project page of archive project
+    And project "Forum" is archived
+    And I visit project "Forum" page
+    Then I should see "Archived"
+
+  Scenario: I should not see archived on projects page with no archived projects
+    And I visit dashboard projects page
+    Then I should not see "Archived"
+
+  Scenario: I should see archived on projects page with archived projects
+    And project "Forum" is archived
+    And I visit dashboard projects page
+    Then I should see "Archived"
+
+  Scenario: I archive project
+    When project "Shop" has push event
+    And I visit project "Shop" page
+    And I visit edit project "Shop" page
+    And I set project archived
+    Then I should see "Archived"
+
+  Scenario: I unarchive project
+    When project "Shop" has push event
+    And project "Shop" is archived
+    And I visit project "Shop" page
+    And I visit edit project "Shop" page
+    And I set project unarchived
+    Then I should not see "Archived"
diff --git a/features/steps/dashboard/dashboard_with_archived_projects.rb b/features/steps/dashboard/dashboard_with_archived_projects.rb
new file mode 100644 (file)
index 0000000..700f4b4
--- /dev/null
@@ -0,0 +1,22 @@
+class DashboardWithArchivedProjects < Spinach::FeatureSteps
+  include SharedAuthentication
+  include SharedPaths
+  include SharedProject
+
+  When 'project "Forum" is archived' do
+    project = Project.find_by_name "Forum"
+    project.update_attribute(:archived, true)
+  end
+
+  Then 'I should see "Shop" project link' do
+    page.should have_link "Shop"
+  end
+
+  Then 'I should not see "Forum" project link' do
+    page.should_not have_link "Forum"
+  end
+
+  Then 'I should see "Forum" project link' do
+    page.should have_link "Forum"
+  end
+end
diff --git a/features/steps/project/project_archived.rb b/features/steps/project/project_archived.rb
new file mode 100644 (file)
index 0000000..149d293
--- /dev/null
@@ -0,0 +1,37 @@
+class ProjectArchived < Spinach::FeatureSteps
+  include SharedAuthentication
+  include SharedProject
+  include SharedPaths
+
+  When 'project "Forum" is archived' do
+    project = Project.find_by_name "Forum"
+    project.update_attribute(:archived, true)
+  end
+
+  When 'project "Shop" is archived' do
+    project = Project.find_by_name "Shop"
+    project.update_attribute(:archived, true)
+  end
+
+  When 'I visit project "Forum" page' do
+    project = Project.find_by_name "Forum"
+    visit project_path(project)
+  end
+
+  Then 'I should not see "Archived"' do
+    page.should_not have_content "Archived"
+  end
+
+  Then 'I should see "Archived"' do
+    page.should have_content "Archived"
+  end
+
+  When 'I set project archived' do
+    click_link "Archive"
+  end
+
+  When 'I set project unarchived' do
+    click_link "Unarchive"
+  end
+
+end
\ No newline at end of file
index cef66b0..3dc4932 100644 (file)
@@ -14,6 +14,13 @@ module SharedProject
     @project.team << [@user, :master]
   end
 
+  # Create another specific project called "Forum"
+  And 'I own project "Forum"' do
+    @project = Project.find_by_name "Forum"
+    @project ||= create(:project_with_code, name: "Forum", namespace: @user.namespace, path: 'forum_project')
+    @project.team << [@user, :master]
+  end
+
   And 'project "Shop" has push event' do
     @project = Project.find_by_name("Shop")
 
index 1e05d18..8aa4c7f 100644 (file)
@@ -21,6 +21,7 @@
 #  imported               :boolean          default(FALSE), not null
 #  import_url             :string(255)
 #  visibility_level       :integer          default(0), not null
+#  archived               :boolean          default(FALSE), not null
 #
 
 require 'spec_helper'
index e8870f4..5f6dff9 100644 (file)
@@ -103,6 +103,33 @@ describe API::API do
       end
     end
 
+    context "archived project" do
+      let(:personal_project) { create(:project, namespace: user.namespace) }
+
+      before do
+        project.team << [user, :developer]
+        project.archive!
+      end
+
+      context "git pull" do
+        it do
+          pull(key, project)
+
+          response.status.should == 200
+          response.body.should == 'true'
+        end
+      end
+
+      context "git push" do
+        it do
+          push(key, project)
+
+          response.status.should == 200
+          response.body.should == 'false'
+        end
+      end
+    end
+
     context "deploy key" do
       let(:key) { create(:deploy_key) }