From fd621429508ba3877e27a8187f0d491576b65ad0 Mon Sep 17 00:00:00 2001
From: Patricio Cano <suprnova32@gmail.com>
Date: Thu, 1 Sep 2016 18:49:48 -0500
Subject: [PATCH] Added group-specific setting for LFS. Groups can
 enable/disable LFS, but this setting can be overridden at the project level.
 Admin only

---
 app/controllers/admin/groups_controller.rb    | 10 ++-
 app/controllers/groups_controller.rb          | 12 +++-
 app/helpers/groups_helper.rb                  |  5 ++
 app/helpers/projects_helper.rb                |  4 +-
 app/models/group.rb                           |  7 ++
 app/models/namespace.rb                       |  5 ++
 app/models/project.rb                         |  9 ++-
 app/views/admin/groups/_form.html.haml        |  2 +
 app/views/admin/groups/show.html.haml         |  7 ++
 app/views/admin/projects/show.html.haml       |  2 +-
 app/views/groups/edit.html.haml               |  2 +
 app/views/shared/_visibility_level.html.haml  |  2 +-
 .../groups/_group_lfs_settings.html.haml      | 11 ++++
 ...901213340_add_lfs_enabled_to_namespaces.rb | 29 ++++++++
 db/schema.rb                                  |  1 +
 doc/api/groups.md                             |  2 +
 lib/api/entities.rb                           |  2 +-
 lib/api/groups.rb                             |  6 +-
 spec/models/project_spec.rb                   | 66 +++++++++++++++++++
 19 files changed, 172 insertions(+), 12 deletions(-)
 create mode 100644 app/views/shared/groups/_group_lfs_settings.html.haml
 create mode 100644 db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb

diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index cdfa8d91a2880..adb8a891c327d 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -60,6 +60,14 @@ def group
   end
 
   def group_params
-    params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level, :request_access_enabled)
+    params.require(:group).permit(
+      :name,
+      :description,
+      :path,
+      :avatar,
+      :visibility_level,
+      :request_access_enabled,
+      :lfs_enabled
+    )
   end
 end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index cb82d62616c86..2f7113aa709e6 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -121,7 +121,17 @@ def determine_layout
   end
 
   def group_params
-    params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock, :request_access_enabled)
+    params.require(:group).permit(
+      :name,
+      :description,
+      :path,
+      :avatar,
+      :public,
+      :visibility_level,
+      :share_with_group_lock,
+      :request_access_enabled,
+      :lfs_enabled
+    )
   end
 
   def load_events
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index b9211e884733f..e87197d2056ad 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -23,4 +23,9 @@ def group_title(group, name = nil, url = nil)
       full_title
     end
   end
+
+  def projects_with_lfs_enabled(group)
+    total = group.projects.size
+    "#{total - group.projects.select{ |p| !p.lfs_enabled? }.size}/#{total} projects have it enabled"
+  end
 end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 16a8e52a4ca91..b4be679c72d8c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -202,8 +202,8 @@ def get_project_nav_tabs(project, current_user)
     nav_tabs.flatten
   end
 
-  def project_lfs_status(project)
-    if project.lfs_enabled?
+  def lfs_status_helper(subject)
+    if subject.lfs_enabled?
       content_tag(:span, class: 'lfs-enabled') do
         'Enabled'
       end
diff --git a/app/models/group.rb b/app/models/group.rb
index c48869ae465ea..aefb94b2adac8 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,6 +95,13 @@ def avatar_url(size = nil)
     end
   end
 
+  def lfs_enabled?
+    return false unless Gitlab.config.lfs.enabled
+    return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+
+    self[:lfs_enabled]
+  end
+
   def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
     user_ids.each do |user_id|
       Member.add_user(
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7c29d27ce9784..919b3b1f095c5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -141,6 +141,11 @@ def find_fork_of(project)
     projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
   end
 
+  def lfs_enabled?
+    # User namespace will always default to the global setting
+    Gitlab.config.lfs.enabled
+  end
+
   private
 
   def repository_storage_paths
diff --git a/app/models/project.rb b/app/models/project.rb
index f3f3ffbbd2829..8e3bedffe1f7c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -393,10 +393,13 @@ def cached_count
   end
 
   def lfs_enabled?
-    return false unless Gitlab.config.lfs.enabled
-    return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+    # Specifically check is lfs_enabled is false
+    return false if self[:lfs_enabled] == false
 
-    self[:lfs_enabled]
+    # Should only fallback to the namespace value if no value is set for the project
+    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
+
+    self[:lfs_enabled] && Gitlab.config.lfs.enabled
   end
 
   def repository_storage_path
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 5f7fdfdb01180..cb631cbfb8df1 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -13,6 +13,8 @@
     .col-sm-offset-2.col-sm-10
       = render 'shared/allow_request_access', form: f
 
+  = render 'shared/groups/group_lfs_settings', f: f
+
   - if @group.new_record?
     .form-group
       .col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index bb37469440076..b8e83075a72f8 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -37,6 +37,13 @@
           %strong
             = @group.created_at.to_s(:medium)
 
+        %li
+          %span.light Group Git LFS status:
+          %strong
+            = lfs_status_helper(@group)
+            = projects_with_lfs_enabled(@group)
+            = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+
     .panel.panel-default
       .panel-heading
         %h3.panel-title
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 6c7c3c48604aa..eecc69b125c0d 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -77,7 +77,7 @@
           %li
             %span.light Git LFS status:
             %strong
-              = project_lfs_status(@project)
+              = lfs_status_helper(@project)
               = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
         - else
           %li
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index decb89b2fd609..849d0f360e70c 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -25,6 +25,8 @@
         .col-sm-offset-2.col-sm-10
           = render 'shared/allow_request_access', form: f
 
+      = render 'shared/groups/group_lfs_settings', f: f
+
       .form-group
         %hr
         = f.label :share_with_group_lock, class: 'control-label' do
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 107ad19177c9e..add4536a0a247 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,7 +1,7 @@
 .form-group.project-visibility-level-holder
   = f.label :visibility_level, class: 'control-label' do
     Visibility Level
-    = link_to "(?)", help_page_path("public_access/public_access")
+    = link_to icon('question-circle'), help_page_path("public_access/public_access")
   .col-sm-10
     - if can_change_visibility_level
       = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
diff --git a/app/views/shared/groups/_group_lfs_settings.html.haml b/app/views/shared/groups/_group_lfs_settings.html.haml
new file mode 100644
index 0000000000000..af57065f0fc59
--- /dev/null
+++ b/app/views/shared/groups/_group_lfs_settings.html.haml
@@ -0,0 +1,11 @@
+- if current_user.admin?
+  .form-group
+    .col-sm-offset-2.col-sm-10
+      .checkbox
+        = f.label :lfs_enabled do
+          = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+          %strong
+            Allow projects within this group to use Git LFS
+            = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+          %br/
+          %span.descr This setting can be overridden in each project.
\ No newline at end of file
diff --git a/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
new file mode 100644
index 0000000000000..9b3ee915e2192
--- /dev/null
+++ b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLfsEnabledToNamespaces < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  # DOWNTIME_REASON = ''
+
+  # When using the methods "add_concurrent_index" or "add_column_with_default"
+  # you must disable the use of transactions as these methods can not run in an
+  # existing transaction. When using "add_concurrent_index" make sure that this
+  # method is the _only_ method called in the migration, any other changes
+  # should go in a separate migration. This ensures that upon failure _only_ the
+  # index creation fails and can be retried or reverted easily.
+  #
+  # To disable transactions uncomment the following line and remove these
+  # comments:
+  # disable_ddl_transaction!
+
+  def change
+    add_column :namespaces, :lfs_enabled, :boolean
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 61873e38113cf..70279f372c967 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -650,6 +650,7 @@
     t.integer  "visibility_level",       default: 20,    null: false
     t.boolean  "request_access_enabled", default: true,  null: false
     t.datetime "deleted_at"
+    t.boolean  "lfs_enabled"
   end
 
   add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
diff --git a/doc/api/groups.md b/doc/api/groups.md
index a898387eaa2a6..cb49648c36ff8 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -288,6 +288,7 @@ Parameters:
 - `path` (required) - The path of the group
 - `description` (optional) - The group's description
 - `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
+- `lfs_enabled` (optional)      - Enable/disable LFS for the projects in this group
 
 ## Transfer project to group
 
@@ -317,6 +318,7 @@ PUT /groups/:id
 | `path` | string | no | The path of the group |
 | `description` | string | no | The description of the group |
 | `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
+| `lfs_enabled` (optional) | boolean | no | Enable/disable LFS for the projects in this group |
 
 ```bash
 curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4f736e4ec2b4b..1529a44f58db9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -120,7 +120,7 @@ class AccessRequester < UserBasic
     end
 
     class Group < Grape::Entity
-      expose :id, :name, :path, :description, :visibility_level
+      expose :id, :name, :path, :description, :visibility_level, :lfs_enabled
       expose :avatar_url
       expose :web_url
     end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index d2df77238d59a..60ac9bdfa33a5 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -27,13 +27,14 @@ class Groups < Grape::API
       #   path (required)             - The path of the group
       #   description (optional)      - The description of the group
       #   visibility_level (optional) - The visibility level of the group
+      #   lfs_enabled (optional)      - Enable/disable LFS for the projects in this group
       # Example Request:
       #   POST /groups
       post do
         authorize! :create_group
         required_attributes! [:name, :path]
 
-        attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+        attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled]
         @group = Group.new(attrs)
 
         if @group.save
@@ -51,13 +52,14 @@ class Groups < Grape::API
       #   path (optional)             - The path of the group
       #   description (optional)      - The description of the group
       #   visibility_level (optional) - The visibility level of the group
+      #   lfs_enabled (optional)      - Enable/disable LFS for the projects in this group
       # Example Request:
       #   PUT /groups/:id
       put ':id' do
         group = find_group(params[:id])
         authorize! :admin_group, group
 
-        attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+        attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled]
 
         if ::Groups::UpdateService.new(group, current_user, attrs).execute
           present group, with: Entities::GroupDetail
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f6e811828fca7..7ca1bd1e5c95e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1417,6 +1417,68 @@ def create_build(new_pipeline = pipeline, name = 'test')
     end
   end
 
+  describe '#lfs_enabled?' do
+    let(:project) { create(:project) }
+
+    shared_examples 'project overrides group' do
+      it 'returns true when enabled in project' do
+        project.update_attribute(:lfs_enabled, true)
+
+        expect(project.lfs_enabled?).to be_truthy
+      end
+
+      it 'returns false when disabled in project' do
+        project.update_attribute(:lfs_enabled, false)
+
+        expect(project.lfs_enabled?).to be_falsey
+      end
+
+      it 'returns the value from the namespace, when no value is set in project' do
+        expect(project.lfs_enabled?).to eq(project.namespace.lfs_enabled?)
+      end
+    end
+
+    context 'LFS disabled in group' do
+      before do
+        project.namespace.update_attribute(:lfs_enabled, false)
+        enable_lfs
+      end
+
+      it_behaves_like 'project overrides group'
+    end
+
+    context 'LFS enabled in group' do
+      before do
+        project.namespace.update_attribute(:lfs_enabled, true)
+        enable_lfs
+      end
+
+      it_behaves_like 'project overrides group'
+    end
+
+    describe 'LFS disabled globally' do
+      shared_examples 'it always returns false' do
+        it do
+          expect(project.lfs_enabled?).to be_falsey
+          expect(project.namespace.lfs_enabled?).to be_falsey
+        end
+      end
+
+      context 'when no values are set' do
+        it_behaves_like 'it always returns false'
+      end
+
+      context 'when all values are set to true' do
+        before do
+          project.namespace.update_attribute(:lfs_enabled, true)
+          project.update_attribute(:lfs_enabled, true)
+        end
+
+        it_behaves_like 'it always returns false'
+      end
+    end
+  end
+
   describe '.where_paths_in' do
     context 'without any paths' do
       it 'returns an empty relation' do
@@ -1581,4 +1643,8 @@ def create_build(new_pipeline = pipeline, name = 'test')
       expect(project.pushes_since_gc).to eq(0)
     end
   end
+
+  def enable_lfs
+    allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+  end
 end
-- 
GitLab