Commit 641d3f0a authored by Mark Chao's avatar Mark Chao

Add RepositorySizeChecker for size calculations

Currently repository size checking is only happening at EE,
and is coupled with attribute names heavily.
By extracting related calculations into RepositorySizeChecker,
they are decoupled from attribute names,
and can later be reused on Snippet which has different attribute names.

This is moved to FOSS level, same level as snippet checking.
parent a6b7f91b
...@@ -111,4 +111,8 @@ module HasRepository ...@@ -111,4 +111,8 @@ module HasRepository
def web_url(only_path: nil) def web_url(only_path: nil)
raise NotImplementedError raise NotImplementedError
end end
def repository_size_checker
raise NotImplementedError
end
end end
...@@ -15,13 +15,13 @@ module EE ...@@ -15,13 +15,13 @@ module EE
override :limit_exceeded? override :limit_exceeded?
def limit_exceeded? def limit_exceeded?
project.above_size_limit? || objects_exceed_repo_limit? size_checker.above_size_limit? || objects_exceed_repo_limit?
end end
def render_size_error def render_size_error
render( render(
json: { json: {
message: ::Gitlab::RepositorySizeError.new(project).push_error(@exceeded_limit), # rubocop:disable Gitlab/ModuleWithInstanceVariables message: size_checker.error_message.push_error(@exceeded_limit), # rubocop:disable Gitlab/ModuleWithInstanceVariables
documentation_url: help_url documentation_url: help_url
}, },
content_type: ::LfsRequest::CONTENT_TYPE, content_type: ::LfsRequest::CONTENT_TYPE,
...@@ -31,16 +31,18 @@ module EE ...@@ -31,16 +31,18 @@ module EE
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def objects_exceed_repo_limit? def objects_exceed_repo_limit?
return false unless project.size_limit_enabled? return false unless size_checker.enabled?
strong_memoize(:limit_exceeded) do strong_memoize(:limit_exceeded) do
lfs_push_size = objects.sum { |o| o[:size] } lfs_push_size = objects.sum { |o| o[:size] }
size_with_lfs_push = project.repository_and_lfs_size + lfs_push_size @exceeded_limit = size_checker.exceeded_size(lfs_push_size) # rubocop:disable Gitlab/ModuleWithInstanceVariables
@exceeded_limit = size_with_lfs_push - project.actual_size_limit # rubocop:disable Gitlab/ModuleWithInstanceVariables
@exceeded_limit > 0 # rubocop:disable Gitlab/ModuleWithInstanceVariables @exceeded_limit > 0 # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def size_checker
project.repository_size_checker
end
end end
end end
...@@ -180,10 +180,6 @@ module EE ...@@ -180,10 +180,6 @@ module EE
"The total size of this project's repository #{show_lfs} will be limited to this size. 0 for unlimited. Leave empty to inherit the group/global value." "The total size of this project's repository #{show_lfs} will be limited to this size. 0 for unlimited. Leave empty to inherit the group/global value."
end end
def project_above_size_limit_message
::Gitlab::RepositorySizeError.new(@project).above_size_limit_message
end
override :membership_locked? override :membership_locked?
def membership_locked? def membership_locked?
group = @project.group group = @project.group
......
...@@ -492,6 +492,16 @@ module EE ...@@ -492,6 +492,16 @@ module EE
::Gitlab::UrlSanitizer.new(bare_url, credentials: { user: import_data&.user }).full_url ::Gitlab::UrlSanitizer.new(bare_url, credentials: { user: import_data&.user }).full_url
end end
def repository_size_checker
strong_memoize(:repository_size_checker) do
::Gitlab::RepositorySizeChecker.new(
current_size_proc: -> { statistics.total_repository_size },
limit: (repository_size_limit || namespace.actual_size_limit),
enabled: License.feature_available?(:repository_size_limit)
)
end
end
def username_only_import_url=(value) def username_only_import_url=(value)
unless ::Gitlab::UrlSanitizer.valid?(value) unless ::Gitlab::UrlSanitizer.valid?(value)
self.import_url = value self.import_url = value
...@@ -508,38 +518,6 @@ module EE ...@@ -508,38 +518,6 @@ module EE
username_only_import_url username_only_import_url
end end
def repository_and_lfs_size
statistics.total_repository_size
end
def above_size_limit?
return false unless size_limit_enabled?
repository_and_lfs_size > actual_size_limit
end
def size_to_remove
repository_and_lfs_size - actual_size_limit
end
def actual_size_limit
return namespace.actual_size_limit if repository_size_limit.nil?
repository_size_limit
end
def size_limit_enabled?
return false unless License.feature_available?(:repository_size_limit)
actual_size_limit != 0
end
def changes_will_exceed_size_limit?(size_in_bytes)
size_limit_enabled? &&
(size_in_bytes > actual_size_limit ||
size_in_bytes + repository_and_lfs_size > actual_size_limit)
end
def remove_import_data def remove_import_data
super unless mirror? super unless mirror?
end end
......
...@@ -15,8 +15,10 @@ module EE ...@@ -15,8 +15,10 @@ module EE
end end
def validate_repository_size! def validate_repository_size!
if project.above_size_limit? size_checker = project.repository_size_checker
raise_error(::Gitlab::RepositorySizeError.new(project).commit_error)
if size_checker.above_size_limit?
raise_error(size_checker.error_message.commit_error)
end end
end end
end end
......
...@@ -49,10 +49,10 @@ module EE ...@@ -49,10 +49,10 @@ module EE
private private
def check_size_limit def check_size_limit
if merge_request.target_project.above_size_limit? size_checker = merge_request.target_project.repository_size_checker
message = ::Gitlab::RepositorySizeError.new(merge_request.target_project).merge_error
raise ::MergeRequests::MergeService::MergeError, message if size_checker.above_size_limit?
raise ::MergeRequests::MergeService::MergeError, size_checker.error_message.merge_error
end end
end end
......
- if project.above_size_limit? - size_checker = project.repository_size_checker
- if size_checker.above_size_limit?
.alert.alert-warning.d-none.d-sm-block .alert.alert-warning.d-none.d-sm-block
= project_above_size_limit_message = size_checker.error_message.above_size_limit_message
- error_messages = Gitlab::RepositorySizeError.new(@project) - error_messages = @project.repository_size_checker.error_message
%h4.size-limit-reached %h4.size-limit-reached
= icon("exclamation-triangle") = icon("exclamation-triangle")
......
...@@ -97,8 +97,8 @@ module EE ...@@ -97,8 +97,8 @@ module EE
end end
def check_size_before_push! def check_size_before_push!
if check_size_limit? && project.above_size_limit? if check_size_limit? && size_checker.above_size_limit?
raise ::Gitlab::GitAccess::ForbiddenError, ::Gitlab::RepositorySizeError.new(project).push_error raise ::Gitlab::GitAccess::ForbiddenError, size_checker.error_message.push_error
end end
end end
...@@ -153,17 +153,21 @@ module EE ...@@ -153,17 +153,21 @@ module EE
end end
def check_size_against_limit(size) def check_size_against_limit(size)
if project.changes_will_exceed_size_limit?(size) if size_checker.changes_will_exceed_size_limit?(size)
raise ::Gitlab::GitAccess::ForbiddenError, ::Gitlab::RepositorySizeError.new(project).new_changes_error raise ::Gitlab::GitAccess::ForbiddenError, size_checker.error_message.new_changes_error
end end
end end
def check_size_limit? def check_size_limit?
strong_memoize(:check_size_limit) do strong_memoize(:check_size_limit) do
project.size_limit_enabled? && size_checker.enabled? &&
changes_list.any? { |change| !::Gitlab::Git.blank_ref?(change[:newrev]) } changes_list.any? { |change| !::Gitlab::Git.blank_ref?(change[:newrev]) }
end end
end end
def size_checker
project.repository_size_checker
end
end end
end end
end end
...@@ -17,7 +17,9 @@ describe ProjectsController do ...@@ -17,7 +17,9 @@ describe ProjectsController do
render_views render_views
it 'shows the over size limit warning message if above_size_limit' do it 'shows the over size limit warning message if above_size_limit' do
allow_any_instance_of(EE::Project).to receive(:above_size_limit?).and_return(true) allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
expect(checker).to receive(:above_size_limit?).and_return(true)
end
allow(controller).to receive(:current_user).and_return(user) allow(controller).to receive(:current_user).and_return(user)
get :show, params: { namespace_id: public_project.namespace.path, id: public_project.path } get :show, params: { namespace_id: public_project.namespace.path, id: public_project.path }
......
...@@ -186,8 +186,8 @@ describe Gitlab::GitAccess do ...@@ -186,8 +186,8 @@ describe Gitlab::GitAccess do
repository.delete_branch('2-mb-file') repository.delete_branch('2-mb-file')
repository.delete_branch('wip') repository.delete_branch('wip')
allow(project).to receive(:repository_and_lfs_size).and_return(repository_size)
project.update_attribute(:repository_size_limit, repository_size_limit) project.update_attribute(:repository_size_limit, repository_size_limit)
allow(project.repository_size_checker).to receive_messages(current_size: repository_size)
end end
shared_examples_for 'a push to repository over the limit' do shared_examples_for 'a push to repository over the limit' do
......
...@@ -969,48 +969,6 @@ describe Project do ...@@ -969,48 +969,6 @@ describe Project do
end end
end end
describe '#size_limit_enabled?' do
let(:project) { create(:project) }
context 'when repository_size_limit is not configured' do
it 'is disabled' do
expect(project.size_limit_enabled?).to be_falsey
end
end
context 'when repository_size_limit is configured' do
before do
project.update(repository_size_limit: 1024)
end
context 'with an EES license' do
let!(:license) { create(:license, plan: License::STARTER_PLAN) }
it 'is enabled' do
expect(project.size_limit_enabled?).to be_truthy
end
end
context 'with an EEP license' do
let!(:license) { create(:license, plan: License::PREMIUM_PLAN) }
it 'is enabled' do
expect(project.size_limit_enabled?).to be_truthy
end
end
context 'without a License' do
before do
License.destroy_all # rubocop: disable DestroyAll
end
it 'is disabled' do
expect(project.size_limit_enabled?).to be_falsey
end
end
end
end
describe '#service_desk_enabled?' do describe '#service_desk_enabled?' do
let!(:license) { create(:license, plan: License::PREMIUM_PLAN) } let!(:license) { create(:license, plan: License::PREMIUM_PLAN) }
let(:namespace) { create(:namespace) } let(:namespace) { create(:namespace) }
...@@ -2121,77 +2079,71 @@ describe Project do ...@@ -2121,77 +2079,71 @@ describe Project do
end end
end end
describe 'repository size restrictions' do describe '#repository_size_checker' do
let(:project) { build(:project) } let(:project) { build(:project) }
let(:checker) { project.repository_size_checker }
before do describe '#current_size' do
allow_any_instance_of(ApplicationSetting).to receive(:repository_size_limit).and_return(50) let(:project) { create(:project) }
end
describe '#changes_will_exceed_size_limit?' do it 'returns the total repository and lfs size' do
before do allow(project.statistics).to receive(:total_repository_size).and_return(80)
allow(project).to receive(:repository_and_lfs_size).and_return(49)
end
it 'returns true when changes go over' do
expect(project.changes_will_exceed_size_limit?(5)).to be_truthy
end
end
describe '#actual_size_limit' do expect(checker.current_size).to eq(80)
it 'returns the limit set in the application settings' do
expect(project.actual_size_limit).to eq(50)
end end
end
it 'returns the value set in the group' do describe '#limit' do
group = create(:group, repository_size_limit: 100) it 'returns the value set in the namespace when available' do
project.update_attribute(:namespace_id, group.id) allow(project.namespace).to receive(:actual_size_limit).and_return(100)
expect(project.actual_size_limit).to eq(100) expect(checker.limit).to eq(100)
end end
it 'returns the value set locally' do it 'returns the value set locally when available' do
project.update_attribute(:repository_size_limit, 75) project.repository_size_limit = 200
expect(project.actual_size_limit).to eq(75) expect(checker.limit).to eq(200)
end end
end end
describe '#size_limit_enabled?' do describe '#enabled?' do
it 'returns false when disabled' do it 'returns true when not equal to zero' do
project.update_attribute(:repository_size_limit, 0) project.repository_size_limit = 1
expect(project.size_limit_enabled?).to be_falsey expect(checker.enabled?).to be_truthy
end end
it 'returns true when a limit is set' do it 'returns false when equals to zero' do
project.update_attribute(:repository_size_limit, 75) project.repository_size_limit = 0
expect(project.size_limit_enabled?).to be_truthy expect(checker.enabled?).to be_falsey
end end
end
describe '#above_size_limit?' do context 'when repository_size_limit is configured' do
let(:project) do before do
create(:project, project.repository_size_limit = 1
statistics: build(:project_statistics)) end
end
it 'returns true when above the limit' do
allow(project).to receive(:repository_and_lfs_size).and_return(100)
expect(project.above_size_limit?).to be_truthy context 'when license feature enabled' do
end before do
stub_licensed_features(repository_size_limit: true)
end
it 'returns false when not over the limit' do it 'is enabled' do
expect(project.above_size_limit?).to be_falsey expect(checker.enabled?).to be_truthy
end end
end end
describe '#size_to_remove' do context 'when license feature disabled' do
it 'returns the correct value' do before do
allow(project).to receive(:repository_and_lfs_size).and_return(100) stub_licensed_features(repository_size_limit: false)
end
expect(project.size_to_remove).to eq(50) it 'is disabled' do
expect(checker.enabled?).to be_falsey
end
end
end end
end end
end end
...@@ -2282,19 +2234,6 @@ describe Project do ...@@ -2282,19 +2234,6 @@ describe Project do
end end
end end
describe '#repository_and_lfs_size' do
let(:project) { create(:project, :repository) }
let(:size) { 50 }
before do
allow(project.statistics).to receive(:total_repository_size).and_return(size)
end
it 'returns the total repository and lfs size' do
expect(project.repository_and_lfs_size).to eq(size)
end
end
describe '#approver_group_ids=' do describe '#approver_group_ids=' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -49,9 +49,13 @@ describe 'Git LFS API and storage' do ...@@ -49,9 +49,13 @@ describe 'Git LFS API and storage' do
context 'and project is above the limit' do context 'and project is above the limit' do
let(:update_lfs_permissions) do let(:update_lfs_permissions) do
allow_any_instance_of(EE::Project).to receive_messages( allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
repository_and_lfs_size: 100.megabytes, allow(checker).to receive_messages(
actual_size_limit: 99.megabytes) enabled?: true,
current_size: 100.megabytes,
limit: 99.megabytes
)
end
end end
it 'responds with status 406' do it 'responds with status 406' do
...@@ -62,9 +66,13 @@ describe 'Git LFS API and storage' do ...@@ -62,9 +66,13 @@ describe 'Git LFS API and storage' do
context 'and project will go over the limit' do context 'and project will go over the limit' do
let(:update_lfs_permissions) do let(:update_lfs_permissions) do
allow_any_instance_of(EE::Project).to receive_messages( allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
repository_and_lfs_size: 200.megabytes, allow(checker).to receive_messages(
actual_size_limit: 300.megabytes) enabled?: true,
current_size: 200.megabytes,
limit: 300.megabytes
)
end
end end
it 'responds with status 406' do it 'responds with status 406' do
...@@ -117,9 +125,9 @@ describe 'Git LFS API and storage' do ...@@ -117,9 +125,9 @@ describe 'Git LFS API and storage' do
context 'and project has limit enabled but will stay under the limit' do context 'and project has limit enabled but will stay under the limit' do
before do before do
allow_any_instance_of(EE::Project).to receive_messages( allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
actual_size_limit: 200, allow(checker).to receive_messages(limit: 200, enabled?: true)
size_limit_enabled?: true) end
put_finalize put_finalize
end end
......
...@@ -18,7 +18,7 @@ describe Commits::CreateService do ...@@ -18,7 +18,7 @@ describe Commits::CreateService do
before do before do
stub_licensed_features(repository_size_limit: true) stub_licensed_features(repository_size_limit: true)
project.update!(repository_size_limit: 1) project.update!(repository_size_limit: 1)
allow(project).to receive(:repository_and_lfs_size).and_return(2) allow(project.repository_size_checker).to receive(:current_size).and_return(2)
end end
subject(:result) { service.execute } subject(:result) { service.execute }
......
...@@ -15,7 +15,7 @@ describe MergeRequests::MergeService do ...@@ -15,7 +15,7 @@ describe MergeRequests::MergeService do
describe '#execute' do describe '#execute' do
context 'project has exceeded size limit' do context 'project has exceeded size limit' do
before do before do
allow(project).to receive(:above_size_limit?).and_return(true) allow(project.repository_size_checker).to receive(:above_size_limit?).and_return(true)
end end
it 'persists the correct error message' do it 'persists the correct error message' do
......
# frozen_string_literal: true
module Gitlab
# Centralized class for repository size related calculations.
class RepositorySizeChecker
attr_reader :limit
def initialize(current_size_proc:, limit:, enabled: true)
@current_size_proc = current_size_proc
@limit = limit
@enabled = enabled && limit != 0
end
def current_size
@current_size ||= @current_size_proc.call
end
def enabled?
@enabled
end
def above_size_limit?
return false unless enabled?
current_size > limit
end
# @param change_size [int] in bytes
def changes_will_exceed_size_limit?(change_size)
return false unless enabled?
change_size > limit || exceeded_size(change_size) > 0
end
# @param change_size [int] in bytes
def exceeded_size(change_size = 0)
current_size + change_size - limit
end
def error_message
@error_message_object ||= Gitlab::RepositorySizeErrorMessage.new(self)
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Gitlab module Gitlab
class RepositorySizeError class RepositorySizeErrorMessage
include ActiveSupport::NumberHelper include ActiveSupport::NumberHelper
attr_reader :project delegate :current_size, :limit, to: :@checker
def initialize(project) # @param checher [RepositorySizeChecker]
@project = project def initialize(checker)
end @checker = checker
def to_s
"The size of this repository (#{current_size}) exceeds the limit of #{limit} by #{size_to_remove}."
end end
def commit_error def commit_error
...@@ -22,12 +19,12 @@ module Gitlab ...@@ -22,12 +19,12 @@ module Gitlab
"This merge request cannot be merged, #{base_message}" "This merge request cannot be merged, #{base_message}"
end end
def push_error(exceeded_limit = nil) def push_error(exceeded_size = nil)
"Your push has been rejected, #{base_message(exceeded_limit)}. #{more_info_message}" "Your push has been rejected, #{base_message(exceeded_size)}. #{more_info_message}"
end end
def new_changes_error def new_changes_error
"Your push to this repository would cause it to exceed the size limit of #{limit} so it has been rejected. #{more_info_message}" "Your push to this repository would cause it to exceed the size limit of #{formatted(limit)} so it has been rejected. #{more_info_message}"
end end
def more_info_message def more_info_message
...@@ -35,28 +32,20 @@ module Gitlab ...@@ -35,28 +32,20 @@ module Gitlab
end end
def above_size_limit_message def above_size_limit_message
"#{self} You won't be able to push new code to this project. #{more_info_message}" "The size of this repository (#{formatted(current_size)}) exceeds the limit of #{formatted(limit)} by #{formatted(size_to_remove)}. You won't be able to push new code to this project. #{more_info_message}"
end end
private private
def base_message(exceeded_limit = nil) def base_message(exceeded_size = nil)
"because this repository has exceeded its size limit of #{limit} by #{size_to_remove(exceeded_limit)}" "because this repository has exceeded its size limit of #{formatted(limit)} by #{formatted(size_to_remove(exceeded_size))}"
end
def current_size
format_number(project.repository_and_lfs_size)
end
def limit
format_number(project.actual_size_limit)
end end
def size_to_remove(exceeded_limit = nil) def size_to_remove(exceeded_size = nil)
format_number(exceeded_limit || project.size_to_remove) exceeded_size || checker.exceeded_size
end end
def format_number(number) def formatted(number)
number_to_human_size(number, delimiter: ',', precision: 2) number_to_human_size(number, delimiter: ',', precision: 2)
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::RepositorySizeChecker do
let(:current_size) { 0 }
let(:limit) { 50 }
let(:enabled) { true }
subject do
described_class.new(
current_size_proc: -> { current_size },
limit: limit,
enabled: enabled
)
end
describe '#enabled?' do
context 'when enabled' do
it 'returns true' do
expect(subject.enabled?).to be_truthy
end
end
context 'when limit is zero' do
let(:limit) { 0 }
it 'returns false' do
expect(subject.enabled?).to be_falsey
end
end
end
describe '#changes_will_exceed_size_limit?' do
let(:current_size) { 49 }
it 'returns true when changes go over' do
expect(subject.changes_will_exceed_size_limit?(2)).to be_truthy
end
it 'returns false when changes do not go over' do
expect(subject.changes_will_exceed_size_limit?(1)).to be_falsey
end
end
describe '#above_size_limit?' do
context 'when size is above the limit' do
let(:current_size) { 100 }
it 'returns true' do
expect(subject.above_size_limit?).to be_truthy
end
end
it 'returns false when not over the limit' do
expect(subject.above_size_limit?).to be_falsey
end
end
describe '#exceeded_size' do
context 'when current size is below or equal to the limit' do
let(:current_size) { 50 }
it 'returns zero' do
expect(subject.exceeded_size).to eq(0)
end
end
context 'when current size is over the limit' do
let(:current_size) { 51 }
it 'returns zero' do
expect(subject.exceeded_size).to eq(1)
end
end
context 'when change size will be over the limit' do
let(:current_size) { 50 }
it 'returns zero' do
expect(subject.exceeded_size(1)).to eq(1)
end
end
context 'when change size will not be over the limit' do
let(:current_size) { 49 }
it 'returns zero' do
expect(subject.exceeded_size(1)).to eq(0)
end
end
end
end
...@@ -2,25 +2,18 @@ ...@@ -2,25 +2,18 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::RepositorySizeError do describe Gitlab::RepositorySizeErrorMessage do
let(:project) do let(:checker) do
create(:project, statistics: build(:project_statistics, repository_size: 15.megabytes)) Gitlab::RepositorySizeChecker.new(
current_size_proc: -> { 15.megabytes },
limit: 10.megabytes
)
end end
let(:message) { described_class.new(project) } let(:message) { checker.error_message }
let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' } let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' }
before do
allow(project).to receive(:actual_size_limit).and_return(10.megabytes)
end
describe 'error messages' do describe 'error messages' do
describe '#to_s' do
it 'returns the correct message' do
expect(message.to_s).to eq('The size of this repository (15 MB) exceeds the limit of 10 MB by 5 MB.')
end
end
describe '#commit_error' do describe '#commit_error' do
it 'returns the correct message' do it 'returns the correct message' do
expect(message.commit_error).to eq("Your changes could not be committed, #{base_message}") expect(message.commit_error).to eq("Your changes could not be committed, #{base_message}")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment