Commit 2d5901c4 authored by James Lopez's avatar James Lopez

Merge branch '1541-deal-with-repo-size-limit-as-byte' into 'master'

Deal with repository size limits as Bytes instead Megabytes

Closes #1541

See merge request !1067
parents ddcc8e7c 1b902a99
...@@ -5,7 +5,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -5,7 +5,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
def update def update
if @application_setting.update_attributes(application_setting_params) result = ::ApplicationSettings::UpdateService.new(@application_setting, current_user, application_setting_params).execute
if result[:status] == :success
redirect_to admin_application_settings_path, redirect_to admin_application_settings_path,
notice: 'Application settings saved successfully' notice: 'Application settings saved successfully'
else else
......
...@@ -136,7 +136,7 @@ module LfsRequest ...@@ -136,7 +136,7 @@ module LfsRequest
size_of_objects = objects.sum { |o| o[:size] } size_of_objects = objects.sum { |o| o[:size] }
@limit_exceeded = (project.repository_and_lfs_size + size_of_objects.to_mb) > project.actual_size_limit @limit_exceeded = (project.repository_and_lfs_size + size_of_objects) > project.actual_size_limit
end end
end end
......
...@@ -1541,8 +1541,10 @@ class Project < ActiveRecord::Base ...@@ -1541,8 +1541,10 @@ class Project < ActiveRecord::Base
actual_size_limit != 0 actual_size_limit != 0
end end
def changes_will_exceed_size_limit?(size_mb) def changes_will_exceed_size_limit?(size_in_bytes)
size_limit_enabled? && (size_mb > actual_size_limit || size_mb + repository_and_lfs_size > actual_size_limit) size_limit_enabled? &&
(size_in_bytes > actual_size_limit ||
size_in_bytes + repository_and_lfs_size > actual_size_limit)
end end
def environments_for(ref, commit: nil, with_tags: false) def environments_for(ref, commit: nil, with_tags: false)
......
module ApplicationSettings
class BaseService < ::BaseService
attr_accessor :application_setting, :current_user, :params
def initialize(application_setting, user, params = {})
@application_setting, @current_user, @params = application_setting, user, params.dup
end
end
end
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
def execute
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(application_setting)
if application_setting.update(params)
success
else
error('Application settings could not be updated')
end
end
end
end
...@@ -46,6 +46,13 @@ class BaseService ...@@ -46,6 +46,13 @@ class BaseService
private private
def assign_repository_size_limit_as_bytes(model)
repository_size_limit = @params.delete(:repository_size_limit)
new_value = repository_size_limit.to_i.megabytes if repository_size_limit.present?
model.repository_size_limit = new_value
end
def error(message, http_status = nil) def error(message, http_status = nil)
result = { result = {
message: message, message: message,
......
...@@ -12,6 +12,9 @@ module Groups ...@@ -12,6 +12,9 @@ module Groups
return @group return @group
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(@group)
if @group.parent && !can?(current_user, :admin_group, @group.parent) if @group.parent && !can?(current_user, :admin_group, @group.parent)
@group.parent = nil @group.parent = nil
@group.errors.add(:parent_id, 'manage access required to create subgroup') @group.errors.add(:parent_id, 'manage access required to create subgroup')
......
...@@ -12,6 +12,9 @@ module Groups ...@@ -12,6 +12,9 @@ module Groups
end end
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(group)
group.assign_attributes(params) group.assign_attributes(params)
begin begin
......
...@@ -22,6 +22,9 @@ module Projects ...@@ -22,6 +22,9 @@ module Projects
return @project return @project
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(@project)
# Set project name from path # Set project name from path
if @project.name.present? && @project.path.present? if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok # if both name and path set - everything is ok
......
...@@ -13,6 +13,9 @@ module Projects ...@@ -13,6 +13,9 @@ module Projects
end end
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(project)
new_branch = params.delete(:default_branch) new_branch = params.delete(:default_branch)
new_repository_storage = params.delete(:repository_storage) new_repository_storage = params.delete(:repository_storage)
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
= f.label :repository_size_limit, class: 'control-label col-sm-2' do = f.label :repository_size_limit, class: 'control-label col-sm-2' do
Size limit per repository (MB) Size limit per repository (MB)
.col-sm-10 .col-sm-10
= f.number_field :repository_size_limit, class: 'form-control', min: 0 = f.number_field :repository_size_limit, value: f.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited. Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited.
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings") = link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings")
......
...@@ -3,6 +3,6 @@ ...@@ -3,6 +3,6 @@
= f.label :repository_size_limit, class: 'control-label' do = f.label :repository_size_limit, class: 'control-label' do
Repository size limit (MB) Repository size limit (MB)
.col-sm-10 .col-sm-10
= f.number_field :repository_size_limit, class: 'form-control', min: 0 = f.number_field :repository_size_limit, value: f.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
= size_limit_message_for_group(@group) = size_limit_message_for_group(@group)
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
.form-group .form-group
= f.label :repository_size_limit, class: 'label-light' do = f.label :repository_size_limit, class: 'label-light' do
Repository size limit (MB) Repository size limit (MB)
= f.number_field :repository_size_limit, class: 'form-control', min: 0 = f.number_field :repository_size_limit, value: f.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
= size_limit_message(@project) = size_limit_message(@project)
......
class ConvertApplicationSettingsRepositorySizeLimitToBytes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
connection.transaction do
rename_column :application_settings, :repository_size_limit, :repository_size_limit_mb
add_column :application_settings, :repository_size_limit, :integer, default: 0, limit: 8
end
bigint_string = if Gitlab::Database.postgresql?
'repository_size_limit_mb::bigint * 1024 * 1024'
else
'repository_size_limit_mb * 1024 * 1024'
end
sql_expression = Arel::Nodes::SqlLiteral.new(bigint_string)
connection.transaction do
update_column_in_batches(:application_settings, :repository_size_limit, sql_expression) do |t, query|
query.where(t[:repository_size_limit_mb].not_eq(nil))
end
remove_column :application_settings, :repository_size_limit_mb
end
end
def down
connection.transaction do
rename_column :application_settings, :repository_size_limit, :repository_size_limit_bytes
add_column :application_settings, :repository_size_limit, :integer, default: 0, limit: nil
end
sql_expression = Arel::Nodes::SqlLiteral.new('repository_size_limit_bytes / 1024 / 1024')
connection.transaction do
update_column_in_batches(:application_settings, :repository_size_limit, sql_expression) do |t, query|
query.where(t[:repository_size_limit_bytes].not_eq(nil))
end
remove_column :application_settings, :repository_size_limit_bytes
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ConvertProjectsRepositorySizeLimitToBytes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
connection.transaction do
rename_column :projects, :repository_size_limit, :repository_size_limit_mb
add_column :projects, :repository_size_limit, :integer, limit: 8
end
bigint_string = if Gitlab::Database.postgresql?
'repository_size_limit_mb::bigint * 1024 * 1024'
else
'repository_size_limit_mb * 1024 * 1024'
end
sql_expression = Arel::Nodes::SqlLiteral.new(bigint_string)
connection.transaction do
update_column_in_batches(:projects, :repository_size_limit, sql_expression) do |t, query|
query.where(t[:repository_size_limit_mb].not_eq(nil))
end
remove_column :projects, :repository_size_limit_mb
end
end
def down
connection.transaction do
rename_column :projects, :repository_size_limit, :repository_size_limit_bytes
add_column :projects, :repository_size_limit, :integer, limit: nil
end
sql_expression = Arel::Nodes::SqlLiteral.new('repository_size_limit_bytes / 1024 / 1024')
connection.transaction do
update_column_in_batches(:projects, :repository_size_limit, sql_expression) do |t, query|
query.where(t[:repository_size_limit_bytes].not_eq(nil))
end
remove_column :projects, :repository_size_limit_bytes
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ConvertNamespacesRepositorySizeLimitToBytes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
connection.transaction do
rename_column :namespaces, :repository_size_limit, :repository_size_limit_mb
add_column :namespaces, :repository_size_limit, :integer, limit: 8
end
bigint_string = if Gitlab::Database.postgresql?
'repository_size_limit_mb::bigint * 1024 * 1024'
else
'repository_size_limit_mb * 1024 * 1024'
end
sql_expression = Arel::Nodes::SqlLiteral.new(bigint_string)
connection.transaction do
update_column_in_batches(:namespaces, :repository_size_limit, sql_expression) do |t, query|
query.where(t[:repository_size_limit_mb].not_eq(nil))
end
remove_column :namespaces, :repository_size_limit_mb
end
end
def down
connection.transaction do
rename_column :namespaces, :repository_size_limit, :repository_size_limit_bytes
add_column :namespaces, :repository_size_limit, :integer, limit: nil
end
sql_expression = Arel::Nodes::SqlLiteral.new('repository_size_limit_bytes / 1024 / 1024')
connection.transaction do
update_column_in_batches(:namespaces, :repository_size_limit, sql_expression) do |t, query|
query.where(t[:repository_size_limit_bytes].not_eq(nil))
end
remove_column :namespaces, :repository_size_limit_bytes
end
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170106172237) do ActiveRecord::Schema.define(version: 20170118200412) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -102,7 +102,6 @@ ActiveRecord::Schema.define(version: 20170106172237) do ...@@ -102,7 +102,6 @@ ActiveRecord::Schema.define(version: 20170106172237) do
t.boolean "usage_ping_enabled", default: true, null: false t.boolean "usage_ping_enabled", default: true, null: false
t.boolean "koding_enabled" t.boolean "koding_enabled"
t.string "koding_url" t.string "koding_url"
t.integer "repository_size_limit", default: 0
t.text "sign_in_text_html" t.text "sign_in_text_html"
t.text "help_page_text_html" t.text "help_page_text_html"
t.text "shared_runners_text_html" t.text "shared_runners_text_html"
...@@ -119,6 +118,7 @@ ActiveRecord::Schema.define(version: 20170106172237) do ...@@ -119,6 +118,7 @@ ActiveRecord::Schema.define(version: 20170106172237) do
t.string "plantuml_url" t.string "plantuml_url"
t.boolean "plantuml_enabled" t.boolean "plantuml_enabled"
t.integer "shared_runners_minutes", default: 0, null: false t.integer "shared_runners_minutes", default: 0, null: false
t.integer "repository_size_limit", limit: 8, default: 0
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
...@@ -853,10 +853,10 @@ ActiveRecord::Schema.define(version: 20170106172237) do ...@@ -853,10 +853,10 @@ ActiveRecord::Schema.define(version: 20170106172237) do
t.datetime "ldap_sync_last_sync_at" t.datetime "ldap_sync_last_sync_at"
t.datetime "deleted_at" t.datetime "deleted_at"
t.boolean "lfs_enabled" t.boolean "lfs_enabled"
t.integer "repository_size_limit"
t.text "description_html" t.text "description_html"
t.integer "parent_id" t.integer "parent_id"
t.integer "shared_runners_minutes_limit" t.integer "shared_runners_minutes_limit"
t.integer "repository_size_limit", limit: 8
end end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
...@@ -1105,8 +1105,8 @@ ActiveRecord::Schema.define(version: 20170106172237) do ...@@ -1105,8 +1105,8 @@ ActiveRecord::Schema.define(version: 20170106172237) do
t.boolean "repository_read_only" t.boolean "repository_read_only"
t.boolean "lfs_enabled" t.boolean "lfs_enabled"
t.text "description_html" t.text "description_html"
t.integer "repository_size_limit"
t.boolean "only_allow_merge_if_all_discussions_are_resolved" t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.integer "repository_size_limit", limit: 8
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
...@@ -185,7 +185,7 @@ module Gitlab ...@@ -185,7 +185,7 @@ module Gitlab
end end
end end
if project.changes_will_exceed_size_limit?(push_size_in_bytes.to_mb) if project.changes_will_exceed_size_limit?(push_size_in_bytes)
raise UnauthorizedError, Gitlab::RepositorySizeError.new(project).new_changes_error raise UnauthorizedError, Gitlab::RepositorySizeError.new(project).new_changes_error
end end
end end
......
...@@ -55,7 +55,7 @@ module Gitlab ...@@ -55,7 +55,7 @@ module Gitlab
end end
def format_number(number) def format_number(number)
number_to_human_size(number * 1.megabyte, delimiter: ',', precision: 2) number_to_human_size(number, delimiter: ',', precision: 2)
end end
end end
end end
...@@ -6,6 +6,36 @@ describe Admin::ApplicationSettingsController do ...@@ -6,6 +6,36 @@ describe Admin::ApplicationSettingsController do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:user) { create(:user)} let(:user) { create(:user)}
describe 'PUT #update' do
before do
sign_in(admin)
end
context 'with valid params' do
subject { put :update, application_setting: { repository_size_limit: '100' } }
it 'redirect to application settings page' do
is_expected.to redirect_to(admin_application_settings_path)
end
it 'set flash notice' do
is_expected.to set_flash[:notice].to('Application settings saved successfully')
end
end
context 'with invalid params' do
subject! { put :update, application_setting: { repository_size_limit: '-100' } }
it 'render show template' do
is_expected.to render_template(:show)
end
it 'assigned @application_settings has errors' do
expect(assigns(:application_setting).errors[:repository_size_limit]).to be_present
end
end
end
describe 'GET #usage_data with no access' do describe 'GET #usage_data with no access' do
before do before do
sign_in(user) sign_in(user)
......
...@@ -751,7 +751,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -751,7 +751,7 @@ describe Gitlab::GitAccess, lib: true do
describe 'repository size restrictions' do describe 'repository size restrictions' do
before do before do
project.update_attribute(:repository_size_limit, 50) project.update_attribute(:repository_size_limit, 50.megabytes)
end end
it 'returns false when blob is too big' do it 'returns false when blob is too big' do
......
...@@ -2,14 +2,14 @@ require 'spec_helper' ...@@ -2,14 +2,14 @@ require 'spec_helper'
describe Gitlab::RepositorySizeError, lib: true do describe Gitlab::RepositorySizeError, lib: true do
let(:project) do let(:project) do
create(:empty_project, statistics: build(:project_statistics, repository_size: 15)) create(:empty_project, statistics: build(:project_statistics, repository_size: 15.megabytes))
end end
let(:message) { Gitlab::RepositorySizeError.new(project) } let(:message) { Gitlab::RepositorySizeError.new(project) }
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 before do
allow(project).to receive(:actual_size_limit).and_return(10) allow(project).to receive(:actual_size_limit).and_return(10.megabytes)
end end
describe 'error messages' do describe 'error messages' do
......
...@@ -176,4 +176,14 @@ describe ApplicationSetting, models: true do ...@@ -176,4 +176,14 @@ describe ApplicationSetting, models: true do
expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
end end
end end
describe '#repository_size_limit column' do
it 'support values up to 8 exabytes' do
setting.update_column(:repository_size_limit, 8.exabytes - 1)
setting.reload
expect(setting.repository_size_limit).to eql(8.exabytes - 1)
end
end
end end
...@@ -103,4 +103,15 @@ describe Group, models: true do ...@@ -103,4 +103,15 @@ describe Group, models: true do
expect(group.actual_size_limit).to eq(75) expect(group.actual_size_limit).to eq(75)
end end
end end
describe '#repository_size_limit column' do
it 'support values up to 8 exabytes' do
group = create(:group)
group.update_column(:repository_size_limit, 8.exabytes - 1)
group.reload
expect(group.repository_size_limit).to eql(8.exabytes - 1)
end
end
end end
...@@ -623,6 +623,17 @@ describe Project, models: true do ...@@ -623,6 +623,17 @@ describe Project, models: true do
end end
end end
describe '#repository_size_limit column' do
it 'support values up to 8 exabytes' do
project = create(:empty_project)
project.update_column(:repository_size_limit, 8.exabytes - 1)
project.reload
expect(project.repository_size_limit).to eql(8.exabytes - 1)
end
end
describe '#default_issues_tracker?' do describe '#default_issues_tracker?' do
it "is true if used internal tracker" do it "is true if used internal tracker" do
project = build(:empty_project) project = build(:empty_project)
......
require 'spec_helper'
describe ApplicationSettings::UpdateService, services: true do
let(:user) { create(:user) }
let(:setting) { ApplicationSetting.create_from_defaults }
let(:service) { described_class.new(setting, user, opts) }
describe '#execute' do
context 'common params' do
let(:opts) { { home_page_url: 'http://foo.bar' } }
it 'properly updates settings with given params' do
service.execute
expect(setting.home_page_url).to eql(opts[:home_page_url])
end
end
context 'with valid params' do
let(:opts) { { repository_size_limit: '100' } }
it 'returns success params' do
result = service.execute
expect(result).to eql(status: :success)
end
end
context 'with invalid params' do
let(:opts) { { repository_size_limit: '-100' } }
it 'returns error params' do
result = service.execute
expect(result).to eql(message: "Application settings could not be updated", status: :error)
end
end
context 'repository_size_limit assignment as Bytes' do
let(:service) { described_class.new(setting, user, opts) }
context 'when param present' do
let(:opts) { { repository_size_limit: '100' } }
it 'converts from MB to Bytes' do
service.execute
expect(setting.reload.repository_size_limit).to eql(100 * 1024 * 1024)
end
end
context 'when param not present' do
let(:opts) { { repository_size_limit: '' } }
it 'does not update due to invalidity' do
service.execute
expect(setting.reload.repository_size_limit).to be_zero
end
it 'assign nil value' do
service.execute
expect(setting.repository_size_limit).to be_nil
end
end
end
end
end
...@@ -40,4 +40,29 @@ describe Groups::CreateService, '#execute', services: true do ...@@ -40,4 +40,29 @@ describe Groups::CreateService, '#execute', services: true do
end end
end end
end end
context 'repository_size_limit assignment as Bytes' do
let(:admin_user) { create(:user, admin: true) }
let(:service) { described_class.new(admin_user, group_params.merge(opts)) }
context 'when param present' do
let(:opts) { { repository_size_limit: '100' } }
it 'assign repository_size_limit as Bytes' do
group = service.execute
expect(group.repository_size_limit).to eql(100 * 1024 * 1024)
end
end
context 'when param not present' do
let(:opts) { { repository_size_limit: '' } }
it 'assign nil value' do
group = service.execute
expect(group.repository_size_limit).to be_nil
end
end
end
end end
...@@ -38,6 +38,31 @@ describe Groups::UpdateService, services: true do ...@@ -38,6 +38,31 @@ describe Groups::UpdateService, services: true do
end end
end end
context 'repository_size_limit assignment as Bytes' do
let(:group) { create(:group, :public, repository_size_limit: 0) }
let(:service) { described_class.new(group, user, opts) }
context 'when param present' do
let(:opts) { { repository_size_limit: '100' } }
it 'converts from MB to Bytes' do
service.execute
expect(group.reload.repository_size_limit).to eql(100 * 1024 * 1024)
end
end
context 'when param not present' do
let(:opts) { { repository_size_limit: '' } }
it 'assign nil value' do
service.execute
expect(group.reload.repository_size_limit).to be_nil
end
end
end
context "unauthorized visibility_level validation" do context "unauthorized visibility_level validation" do
let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } let!(:service) { described_class.new(internal_group, user, visibility_level: 99) }
before do before do
......
...@@ -98,6 +98,30 @@ describe Projects::CreateService, '#execute', services: true do ...@@ -98,6 +98,30 @@ describe Projects::CreateService, '#execute', services: true do
end end
end end
context 'repository_size_limit assignment as Bytes' do
let(:admin_user) { create(:user, admin: true) }
context 'when param present' do
let(:opts) { { repository_size_limit: '100' } }
it 'assign repository_size_limit as Bytes' do
project = create_project(admin_user, opts)
expect(project.repository_size_limit).to eql(100 * 1024 * 1024)
end
end
context 'when param not present' do
let(:opts) { { repository_size_limit: '' } }
it 'assign nil value' do
project = create_project(admin_user, opts)
expect(project.repository_size_limit).to be_nil
end
end
end
context 'restricted visibility level' do context 'restricted visibility level' do
before do before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
......
...@@ -127,6 +127,31 @@ describe Projects::UpdateService, services: true do ...@@ -127,6 +127,31 @@ describe Projects::UpdateService, services: true do
end end
end end
context 'repository_size_limit assignment as Bytes' do
let(:admin_user) { create(:user, admin: true) }
let(:project) { create(:empty_project, repository_size_limit: 0) }
context 'when param present' do
let(:opts) { { repository_size_limit: '100' } }
it 'converts from MB to Bytes' do
update_project(project, admin_user, opts)
expect(project.reload.repository_size_limit).to eql(100 * 1024 * 1024)
end
end
context 'when param not present' do
let(:opts) { { repository_size_limit: '' } }
it 'assign nil value' do
update_project(project, admin_user, opts)
expect(project.reload.repository_size_limit).to be_nil
end
end
end
it 'returns an error result when record cannot be updated' do it 'returns an error result when record cannot be updated' do
result = update_project(project, admin, { name: 'foo&bar' }) result = update_project(project, admin, { name: 'foo&bar' })
......
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