diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js index 418cc69bf5a..08c32944181 100644 --- a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js +++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js @@ -5,7 +5,7 @@ import { formatNumber } from '~/locale'; * * @param {Number} number to be converted * - * @param {options.maxCharLength} Max output char length at the + * @param {options.maxLength} Max output char length at the * expense of precision, if the output is longer than this, * the formatter switches to using exponential notation. * @@ -16,16 +16,35 @@ import { formatNumber } from '~/locale'; * `formatNumber` such as `valueFactor`, `unit` and `style`. * */ -const formatNumberNormalized = (value, { maxCharLength, valueFactor = 1, ...options }) => { +const formatNumberNormalized = (value, { maxLength, valueFactor = 1, ...options }) => { const formatted = formatNumber(value * valueFactor, options); - if (maxCharLength !== undefined && formatted.length > maxCharLength) { + if (maxLength !== undefined && formatted.length > maxLength) { // 123456 becomes 1.23e+8 return value.toExponential(2); } return formatted; }; +/** + * This function converts the old positional arguments into an options + * object. + * + * This is done so we can support legacy fractionDigits and maxLength as positional + * arguments, as well as the better options object. + * + * @param {Object|Number} options + * @returns {Object} options given to the formatter + */ +const getFormatterArguments = (options) => { + if (typeof options === 'object' && options !== null) { + return options; + } + return { + maxLength: options, + }; +}; + /** * Formats a number as a string scaling it up according to units. * @@ -40,7 +59,9 @@ const scaledFormatter = (units, unitFactor = 1000) => { return new RangeError(`unitFactor cannot have the value 0.`); } - return (value, fractionDigits) => { + return (value, fractionDigits, options) => { + const { maxLength, unitSeparator = '' } = getFormatterArguments(options); + if (value === null) { return ''; } @@ -66,11 +87,13 @@ const scaledFormatter = (units, unitFactor = 1000) => { } const unit = units[scale]; + const length = maxLength !== undefined ? maxLength - unit.length : undefined; return `${formatNumberNormalized(num, { + maxLength: length, maximumFractionDigits: fractionDigits, minimumFractionDigits: fractionDigits, - })}${unit}`; + })}${unitSeparator}${unit}`; }; }; @@ -78,14 +101,16 @@ const scaledFormatter = (units, unitFactor = 1000) => { * Returns a function that formats a number as a string. */ export const numberFormatter = (style = 'decimal', valueFactor = 1) => { - return (value, fractionDigits, maxCharLength) => { - return `${formatNumberNormalized(value, { - maxCharLength, + return (value, fractionDigits, options) => { + const { maxLength } = getFormatterArguments(options); + + return formatNumberNormalized(value, { + maxLength, valueFactor, style, maximumFractionDigits: fractionDigits, minimumFractionDigits: fractionDigits, - })}`; + }); }; }; @@ -93,15 +118,16 @@ export const numberFormatter = (style = 'decimal', valueFactor = 1) => { * Returns a function that formats a number as a string with a suffix. */ export const suffixFormatter = (unit = '', valueFactor = 1) => { - return (value, fractionDigits, maxCharLength) => { - const length = maxCharLength !== undefined ? maxCharLength - unit.length : undefined; + return (value, fractionDigits, options) => { + const { maxLength, unitSeparator = '' } = getFormatterArguments(options); + const length = maxLength !== undefined ? maxLength - unit.length : undefined; return `${formatNumberNormalized(value, { - maxCharLength: length, + maxLength: length, valueFactor, maximumFractionDigits: fractionDigits, minimumFractionDigits: fractionDigits, - })}${unit}`; + })}${unitSeparator}${unit}`; }; }; diff --git a/app/assets/javascripts/lib/utils/unit_format/index.js b/app/assets/javascripts/lib/utils/unit_format/index.js index bc82c6aa74d..5c5210027e4 100644 --- a/app/assets/javascripts/lib/utils/unit_format/index.js +++ b/app/assets/javascripts/lib/utils/unit_format/index.js @@ -126,9 +126,11 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => { * * @function * @param {Number} value - Number to format - * @param {Number} fractionDigits - precision decimals - * @param {Number} maxLength - Max length of formatted number + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const number = getFormatter(SUPPORTED_FORMATS.number); @@ -137,9 +139,11 @@ export const number = getFormatter(SUPPORTED_FORMATS.number); * * @function * @param {Number} value - Number to format, `1` is rendered as `100%` - * @param {Number} fractionDigits - number of precision decimals - * @param {Number} maxLength - Max length of formatted number + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const percent = getFormatter(SUPPORTED_FORMATS.percent); @@ -148,9 +152,11 @@ export const percent = getFormatter(SUPPORTED_FORMATS.percent); * * @function * @param {Number} value - Number to format, `100` is rendered as `100%` - * @param {Number} fractionDigits - number of precision decimals - * @param {Number} maxLength - Max length of formatted number + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const percentHundred = getFormatter(SUPPORTED_FORMATS.percentHundred); @@ -159,9 +165,11 @@ export const percentHundred = getFormatter(SUPPORTED_FORMATS.percentHundred); * * @function * @param {Number} value - Number to format, `1` is rendered as `1s` - * @param {Number} fractionDigits - number of precision decimals - * @param {Number} maxLength - Max length of formatted number + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const seconds = getFormatter(SUPPORTED_FORMATS.seconds); @@ -170,9 +178,11 @@ export const seconds = getFormatter(SUPPORTED_FORMATS.seconds); * * @function * @param {Number} value - Number to format, `1` is formatted as `1ms` - * @param {Number} fractionDigits - number of precision decimals - * @param {Number} maxLength - Max length of formatted number + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const milliseconds = getFormatter(SUPPORTED_FORMATS.milliseconds); @@ -182,7 +192,11 @@ export const milliseconds = getFormatter(SUPPORTED_FORMATS.milliseconds); * * @function * @param {Number} value - Number to format, `1` is formatted as `1B` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const decimalBytes = getFormatter(SUPPORTED_FORMATS.decimalBytes); @@ -192,7 +206,11 @@ export const decimalBytes = getFormatter(SUPPORTED_FORMATS.decimalBytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1kB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const kilobytes = getFormatter(SUPPORTED_FORMATS.kilobytes); @@ -202,7 +220,11 @@ export const kilobytes = getFormatter(SUPPORTED_FORMATS.kilobytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1MB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const megabytes = getFormatter(SUPPORTED_FORMATS.megabytes); @@ -212,7 +234,11 @@ export const megabytes = getFormatter(SUPPORTED_FORMATS.megabytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1GB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const gigabytes = getFormatter(SUPPORTED_FORMATS.gigabytes); @@ -222,7 +248,11 @@ export const gigabytes = getFormatter(SUPPORTED_FORMATS.gigabytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1GB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const terabytes = getFormatter(SUPPORTED_FORMATS.terabytes); @@ -232,7 +262,11 @@ export const terabytes = getFormatter(SUPPORTED_FORMATS.terabytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1PB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const petabytes = getFormatter(SUPPORTED_FORMATS.petabytes); @@ -242,7 +276,11 @@ export const petabytes = getFormatter(SUPPORTED_FORMATS.petabytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1B` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const bytes = getFormatter(SUPPORTED_FORMATS.bytes); @@ -252,7 +290,11 @@ export const bytes = getFormatter(SUPPORTED_FORMATS.bytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1kB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const kibibytes = getFormatter(SUPPORTED_FORMATS.kibibytes); @@ -262,7 +304,11 @@ export const kibibytes = getFormatter(SUPPORTED_FORMATS.kibibytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1MB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const mebibytes = getFormatter(SUPPORTED_FORMATS.mebibytes); @@ -272,7 +318,11 @@ export const mebibytes = getFormatter(SUPPORTED_FORMATS.mebibytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1GB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const gibibytes = getFormatter(SUPPORTED_FORMATS.gibibytes); @@ -282,7 +332,11 @@ export const gibibytes = getFormatter(SUPPORTED_FORMATS.gibibytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1GB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const tebibytes = getFormatter(SUPPORTED_FORMATS.tebibytes); @@ -292,7 +346,11 @@ export const tebibytes = getFormatter(SUPPORTED_FORMATS.tebibytes); * * @function * @param {Number} value - Number to format, `1` is formatted as `1PB` - * @param {Number} fractionDigits - number of precision decimals + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - number of precision decimals + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const pebibytes = getFormatter(SUPPORTED_FORMATS.pebibytes); @@ -301,6 +359,10 @@ export const pebibytes = getFormatter(SUPPORTED_FORMATS.pebibytes); * * @function * @param {Number} value - Value to format - * @param {Number} fractionDigits - precision decimals - Defaults to 2 + * @param {Object} options - Formatting options + * @param {Number} options.fractionDigits - precision decimals, defaults to 2 + * @param {Number} options.maxLength - Max length of formatted number + * if length is exceeded, exponential format is used. + * @param {String} options.unitSeparator - Separator between value and unit */ export const engineering = getFormatter(); diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb index a9cd67550dc..ab458bb2c27 100644 --- a/app/models/integrations/emails_on_push.rb +++ b/app/models/integrations/emails_on_push.rb @@ -13,9 +13,7 @@ module Integrations validate :number_of_recipients_within_limit, if: :validate_recipients? def self.valid_recipients(recipients) - recipients.split.select do |recipient| - recipient.include?('@') - end.uniq(&:downcase) + recipients.split.grep(Devise.email_regexp).uniq(&:downcase) end def title diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 3e19f294253..995c26d7221 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -82,10 +82,6 @@ class ProjectMember < Member source end - def owner? - project.owner == user - end - def notifiable_options { project: project } end @@ -132,7 +128,10 @@ class ProjectMember < Member end def post_create_hook - unless owner? + # The creator of a personal project gets added as a `ProjectMember` + # with `OWNER` access during creation of a personal project, + # but we do not want to trigger notifications to the same person who created the personal project. + unless project.personal_namespace_holder?(user) event_service.join_project(self.project, self.user) run_after_commit_or_now { notification_service.new_project_member(self) } end diff --git a/app/models/project.rb b/app/models/project.rb index ba2e4b14042..ecbb71806b8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -899,6 +899,18 @@ class Project < ApplicationRecord association(:namespace).loaded? end + def personal_namespace_holder?(user) + return false unless personal? + return false unless user + + # We do not want to use a check like `project.team.owner?(user)` + # here because that would depend upon the state of the `project_authorizations` cache, + # and also perform the check across multiple `owners` of the project, but our intention + # is to check if the user is the "holder" of the personal namespace, so need to make this + # check against only a single user (ie, namespace.owner). + namespace.owner == user + end + def project_setting super.presence || build_project_setting end diff --git a/app/services/members/projects/creator_service.rb b/app/services/members/projects/creator_service.rb index 4dba81acf73..7d62e2705bc 100644 --- a/app/services/members/projects/creator_service.rb +++ b/app/services/members/projects/creator_service.rb @@ -10,12 +10,12 @@ module Members private def can_update_member? - super || current_user.can?(:update_project_member, member) || adding_a_new_owner? + super || current_user.can?(:update_project_member, member) || adding_the_creator_as_owner_in_a_personal_project? end - def adding_a_new_owner? + def adding_the_creator_as_owner_in_a_personal_project? # this condition is reached during testing setup a lot due to use of `.add_user` - member.owner? && member.new_record? + member.project.personal_namespace_holder?(member.user) && member.new_record? end end end diff --git a/config/initializers/multi_json.rb b/config/initializers/multi_json.rb index 93a81d8320d..5f98ce1961f 100644 --- a/config/initializers/multi_json.rb +++ b/config/initializers/multi_json.rb @@ -2,4 +2,4 @@ # Explicitly set the JSON adapter used by MultiJson # Currently we want this to default to the existing json gem -MultiJson.use(:json_gem) +MultiJson.use(:oj) diff --git a/db/fixtures/development/02_users.rb b/db/fixtures/development/02_users.rb index 76a00c18649..03e4605e729 100644 --- a/db/fixtures/development/02_users.rb +++ b/db/fixtures/development/02_users.rb @@ -4,8 +4,8 @@ class Gitlab::Seeder::Users include ActionView::Helpers::NumberHelper RANDOM_USERS_COUNT = 20 + MASS_NAMESPACES_COUNT = 100 MASS_USERS_COUNT = ENV['CI'] ? 10 : 1_000_000 - attr_reader :opts def initialize(opts = {}) @@ -15,6 +15,7 @@ class Gitlab::Seeder::Users def seed! Sidekiq::Testing.inline! do create_mass_users! + create_mass_namespaces! create_random_users! end end @@ -26,20 +27,22 @@ class Gitlab::Seeder::Users Gitlab::Seeder.with_mass_insert(MASS_USERS_COUNT, User) do ActiveRecord::Base.connection.execute <<~SQL - INSERT INTO users (username, name, email, confirmed_at, projects_limit, encrypted_password) + INSERT INTO users (username, name, email, state, confirmed_at, projects_limit, encrypted_password) SELECT '#{Gitlab::Seeder::MASS_INSERT_USER_START}' || seq, 'Seed user ' || seq, 'seed_user' || seq || '@example.com', + 'active', to_timestamp(seq), #{MASS_USERS_COUNT}, '#{encrypted_password}' FROM generate_series(1, #{MASS_USERS_COUNT}) AS seq + ON CONFLICT DO NOTHING; SQL end relation = User.where(admin: false) - Gitlab::Seeder.with_mass_insert(relation.count, Namespace) do + Gitlab::Seeder.with_mass_insert(relation.count, 'user namespaces') do ActiveRecord::Base.connection.execute <<~SQL INSERT INTO namespaces (name, path, owner_id, type) SELECT @@ -48,6 +51,16 @@ class Gitlab::Seeder::Users id, 'User' FROM users WHERE NOT admin + ON CONFLICT DO NOTHING; + SQL + end + + Gitlab::Seeder.with_mass_insert(relation.count, "User namespaces routes") do + ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO routes (namespace_id, source_id, source_type, path, name) + SELECT id as namespace_id, id as source_id, 'Namespace', path, name + FROM namespaces WHERE type IS NULL OR type = 'User' + ON CONFLICT DO NOTHING; SQL end @@ -74,6 +87,97 @@ class Gitlab::Seeder::Users end end + def create_mass_namespaces! + Gitlab::Seeder.with_mass_insert(MASS_NAMESPACES_COUNT, "root namespaces and subgroups 9 levels deep") do + ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO namespaces (name, path, type) + SELECT + 'mass insert group level 0 - ' || seq, + '#{Gitlab::Seeder::MASS_INSERT_GROUP_START}_0_' || seq, + 'Group' + FROM generate_series(1, #{MASS_NAMESPACES_COUNT}) AS seq + ON CONFLICT DO NOTHING; + SQL + + (1..9).each do |idx| + count = Namespace.where("path LIKE '#{Gitlab::Seeder::MASS_INSERT_PREFIX}%'").where(type: 'Group').count * 2 + Gitlab::Seeder.log_message("Creating subgroups at level #{idx}: #{count}") + ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO namespaces (name, path, type, parent_id) + SELECT + 'mass insert group level #{idx} - ' || seq, + '#{Gitlab::Seeder::MASS_INSERT_GROUP_START}_#{idx}_' || seq, + 'Group', + namespaces.id + FROM namespaces + CROSS JOIN generate_series(1, 2) AS seq + WHERE namespaces.type='Group' AND namespaces.path like '#{Gitlab::Seeder::MASS_INSERT_GROUP_START}_#{idx-1}_%' + ON CONFLICT DO NOTHING; + SQL + end + + Gitlab::Seeder.log_message("creating routes.") + ActiveRecord::Base.connection.execute <<~SQL + WITH RECURSIVE cte(source_id, namespace_id, parent_id, path, height) AS ( + ( + SELECT ARRAY[batch.id], batch.id, batch.parent_id, batch.path, 1 + FROM + "namespaces" as batch + WHERE + "batch"."type" = 'Group' AND "batch"."parent_id" is null + ) + UNION + ( + SELECT array_append(cte.source_id, n.id), n.id, n.parent_id, cte.path || '/' || n.path, cte.height+1 + FROM + "namespaces" as n, + "cte" + WHERE + "n"."type" = 'Group' + AND "n"."parent_id" = "cte"."namespace_id" + ) + ) + INSERT INTO routes (namespace_id, source_id, source_type, path, name) + SELECT cte.namespace_id as namespace_id, cte.namespace_id as source_id, 'Namespace', cte.path, cte.path FROM cte + ON CONFLICT DO NOTHING; + SQL + + Gitlab::Seeder.log_message("filling traversal ids.") + ActiveRecord::Base.connection.execute <<~SQL + WITH RECURSIVE cte(source_id, namespace_id, parent_id) AS ( + ( + SELECT ARRAY[batch.id], batch.id, batch.parent_id + FROM + "namespaces" as batch + WHERE + "batch"."type" = 'Group' AND "batch"."parent_id" is null + ) + UNION + ( + SELECT array_append(cte.source_id, n.id), n.id, n.parent_id + FROM + "namespaces" as n, + "cte" + WHERE + "n"."type" = 'Group' + AND "n"."parent_id" = "cte"."namespace_id" + ) + ) + UPDATE namespaces + SET traversal_ids = computed.source_id FROM (SELECT namespace_id, source_id FROM cte) AS computed + where computed.namespace_id = namespaces.id AND namespaces.path LIKE '#{Gitlab::Seeder::MASS_INSERT_PREFIX}%' + SQL + + Gitlab::Seeder.log_message("creating namespace settings.") + ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO namespace_settings(namespace_id, created_at, updated_at) + SELECT id, now(), now() FROM namespaces + ON CONFLICT DO NOTHING; + SQL + end + end + + def random_password @random_password ||= SecureRandom.hex.slice(0,16) end diff --git a/db/fixtures/development/03_project.rb b/db/fixtures/development/03_project.rb index e879db84e68..ae9a17b637c 100644 --- a/db/fixtures/development/03_project.rb +++ b/db/fixtures/development/03_project.rb @@ -53,14 +53,56 @@ class Gitlab::Seeder::Projects public: 1 # 1m projects = 5m total } + BATCH_SIZE = 100_000 + def seed! Sidekiq::Testing.inline! do create_real_projects! create_large_projects! - create_mass_projects! end end + def self.insert_project_namespaces_sql(type:, range:) + <<~SQL + INSERT INTO namespaces (name, path, parent_id, owner_id, type, visibility_level, created_at, updated_at) + SELECT + 'Seed project ' || seq || ' ' || ('{#{Gitlab::Seeder::Projects.visibility_per_user}}'::text[])[seq] AS project_name, + '#{Gitlab::Seeder::MASS_INSERT_PROJECT_START}' || ('{#{Gitlab::Seeder::Projects.visibility_per_user}}'::text[])[seq] || '_' || seq AS namespace_path, + n.id AS parent_id, + n.owner_id AS owner_id, + 'Project' AS type, + ('{#{Gitlab::Seeder::Projects.visibility_level_per_user}}'::int[])[seq] AS visibility_level, + NOW() AS created_at, + NOW() AS updated_at + FROM namespaces n + CROSS JOIN generate_series(1, #{Gitlab::Seeder::Projects.projects_per_user_count}) AS seq + WHERE type='#{type}' AND path LIKE '#{Gitlab::Seeder::MASS_INSERT_PREFIX}%' + AND n.id BETWEEN #{range.first} AND #{range.last} + ON CONFLICT DO NOTHING; + SQL + end + + def self.insert_projects_sql(type:, range:) + <<~SQL + INSERT INTO projects (name, path, creator_id, namespace_id, project_namespace_id, visibility_level, created_at, updated_at) + SELECT + n.name AS project_name, + n.path AS project_path, + n.owner_id AS creator_id, + n.parent_id AS namespace_id, + n.id AS project_namespace_id, + n.visibility_level AS visibility_level, + NOW() AS created_at, + NOW() AS updated_at + FROM namespaces n + WHERE type = 'Project' AND n.parent_id IN ( + SELECT id FROM namespaces n1 WHERE type='#{type}' + AND path LIKE '#{Gitlab::Seeder::MASS_INSERT_PREFIX}%' AND n1.id BETWEEN #{range.first} AND #{range.last} + ) + ON CONFLICT DO NOTHING; + SQL + end + private def create_real_projects! @@ -156,55 +198,26 @@ class Gitlab::Seeder::Projects end end - def create_mass_projects! - projects_per_user_count = MASS_PROJECTS_COUNT_PER_USER.values.sum - visibility_per_user = ['private'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:private) + - ['internal'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:internal) + - ['public'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:public) - visibility_level_per_user = visibility_per_user.map { |visibility| Gitlab::VisibilityLevel.level_value(visibility) } + def self.projects_per_user_count + MASS_PROJECTS_COUNT_PER_USER.values.sum + end - visibility_per_user = visibility_per_user.join(',') - visibility_level_per_user = visibility_level_per_user.join(',') + def self.visibility_per_user_array + ['private'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:private) + + ['internal'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:internal) + + ['public'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:public) + end - Gitlab::Seeder.with_mass_insert(User.count * projects_per_user_count, "Projects and relations") do - ActiveRecord::Base.connection.execute <<~SQL - INSERT INTO projects (name, path, creator_id, namespace_id, visibility_level, created_at, updated_at) - SELECT - 'Seed project ' || seq || ' ' || ('{#{visibility_per_user}}'::text[])[seq] AS project_name, - '#{Gitlab::Seeder::MASS_INSERT_PROJECT_START}' || ('{#{visibility_per_user}}'::text[])[seq] || '_' || seq AS project_path, - u.id AS user_id, - n.id AS namespace_id, - ('{#{visibility_level_per_user}}'::int[])[seq] AS visibility_level, - NOW() AS created_at, - NOW() AS updated_at - FROM users u - CROSS JOIN generate_series(1, #{projects_per_user_count}) AS seq - JOIN namespaces n ON n.owner_id=u.id - SQL + def self.visibility_level_per_user_map + visibility_per_user_array.map { |visibility| Gitlab::VisibilityLevel.level_value(visibility) } + end - ActiveRecord::Base.connection.execute <<~SQL - INSERT INTO project_features (project_id, merge_requests_access_level, issues_access_level, wiki_access_level, - pages_access_level) - SELECT - id, - #{ProjectFeature::ENABLED} AS merge_requests_access_level, - #{ProjectFeature::ENABLED} AS issues_access_level, - #{ProjectFeature::ENABLED} AS wiki_access_level, - #{ProjectFeature::ENABLED} AS pages_access_level - FROM projects ON CONFLICT (project_id) DO NOTHING; - SQL + def self.visibility_per_user + visibility_per_user_array.join(',') + end - ActiveRecord::Base.connection.execute <<~SQL - INSERT INTO routes (source_id, source_type, name, path) - SELECT - p.id, - 'Project', - u.name || ' / ' || p.name, - u.username || '/' || p.path - FROM projects p JOIN users u ON u.id=p.creator_id - ON CONFLICT (source_type, source_id) DO NOTHING; - SQL - end + def self.visibility_level_per_user + visibility_level_per_user_map.join(',') end end diff --git a/db/fixtures/development/03_project_1_user_projects.rb b/db/fixtures/development/03_project_1_user_projects.rb new file mode 100644 index 00000000000..0b7ac5396b8 --- /dev/null +++ b/db/fixtures/development/03_project_1_user_projects.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Gitlab::Seeder::UserProjects + def seed! + create_user_projects! + end + + private + + def create_user_projects! + user_namespaces = Namespace.where("path LIKE ?", "#{Gitlab::Seeder::MASS_INSERT_PREFIX}%").where(type: 'User') + + Gitlab::Seeder.with_mass_insert(user_namespaces.count * Gitlab::Seeder::Projects.projects_per_user_count, "User projects and corresponding project namespaces") do + user_namespaces.each_batch(of: Gitlab::Seeder::Projects::BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first + count = index * batch.size * Gitlab::Seeder::Projects.projects_per_user_count + + Gitlab::Seeder.log_message("Creating project namespaces: #{count}.") + ActiveRecord::Base.connection.execute(Gitlab::Seeder::Projects.insert_project_namespaces_sql(type: 'User', range: range)) + + Gitlab::Seeder.log_message("Creating projects: #{count}.") + ActiveRecord::Base.connection.execute(Gitlab::Seeder::Projects.insert_projects_sql(type: 'User', range: range)) + end + end + end +end + +Gitlab::Seeder.quiet do + projects = Gitlab::Seeder::UserProjects.new + projects.seed! +end diff --git a/db/fixtures/development/03_project_2_group_projects.rb b/db/fixtures/development/03_project_2_group_projects.rb new file mode 100644 index 00000000000..fe44a1974cf --- /dev/null +++ b/db/fixtures/development/03_project_2_group_projects.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Gitlab::Seeder::GroupProjects + def seed! + create_projects! + end + + private + + def create_projects! + groups = Namespace.where("path LIKE ?", "#{Gitlab::Seeder::MASS_INSERT_PREFIX}%").where(type: 'Group') + + Gitlab::Seeder.with_mass_insert(groups.count * Gitlab::Seeder::Projects.projects_per_user_count, "Projects and corresponding project namespaces") do + groups.each_batch(of: Gitlab::Seeder::Projects::BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first + count = index * batch.size * Gitlab::Seeder::Projects.projects_per_user_count + + Gitlab::Seeder.log_message("Creating projects namespaces: #{count}.") + ActiveRecord::Base.connection.execute(Gitlab::Seeder::Projects.insert_project_namespaces_sql(type: 'Group', range: range)) + + Gitlab::Seeder.log_message("Creating projects: #{count}.") + ActiveRecord::Base.connection.execute(Gitlab::Seeder::Projects.insert_projects_sql(type: 'Group', range: range)) + end + end + end +end + +Gitlab::Seeder.quiet do + projects = Gitlab::Seeder::GroupProjects.new + projects.seed! +end diff --git a/db/fixtures/development/03_project_3_features.rb b/db/fixtures/development/03_project_3_features.rb new file mode 100644 index 00000000000..8b910af2c6c --- /dev/null +++ b/db/fixtures/development/03_project_3_features.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class Gitlab::Seeder::ProjectFeatures + include ActionView::Helpers::NumberHelper + + BATCH_SIZE = 100_000 + + def seed! + create_project_features! + end + + def create_project_features! + Gitlab::Seeder.with_mass_insert(Project.count, "Project features") do + Project.each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first + count = index * BATCH_SIZE + + Gitlab::Seeder.log_message("Creating project features: #{count}.") + ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO project_features (project_id, merge_requests_access_level, issues_access_level, wiki_access_level, pages_access_level) + SELECT + id, + #{ProjectFeature::ENABLED} AS merge_requests_access_level, + #{ProjectFeature::ENABLED} AS issues_access_level, + #{ProjectFeature::ENABLED} AS wiki_access_level, + #{ProjectFeature::ENABLED} AS pages_access_level + FROM projects + WHERE projects.id BETWEEN #{range.first} AND #{range.last} + ON CONFLICT DO NOTHING; + SQL + end + end + end +end + +Gitlab::Seeder.quiet do + projects = Gitlab::Seeder::ProjectFeatures.new + projects.seed! +end diff --git a/db/fixtures/development/03_project_4_routes.rb b/db/fixtures/development/03_project_4_routes.rb new file mode 100644 index 00000000000..66da34ce83c --- /dev/null +++ b/db/fixtures/development/03_project_4_routes.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Gitlab::Seeder::ProjectRoutes + include ActionView::Helpers::NumberHelper + + BATCH_SIZE = 100_000 + + def seed! + create_project_routes! + end + + def create_project_routes! + Gitlab::Seeder.with_mass_insert(Project.count, "Project routes") do + Project.each_batch(of: BATCH_SIZE / 2) do |batch, index| + range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first + count = index * BATCH_SIZE / 2 + + Gitlab::Seeder.log_message("Creating project routes: #{count}.") + ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO routes (namespace_id, source_id, source_type, name, path) + SELECT + p.project_namespace_id as namespace_id, + p.id as source_id, + 'Project', + routes.name || ' / ' || p.name, + routes.path || '/' || p.path + FROM projects p + INNER JOIN routes ON routes.source_id = p.namespace_id and source_type = 'Namespace' + WHERE p.id BETWEEN #{range.first} AND #{range.last} + ON CONFLICT DO NOTHING; + SQL + end + end + end +end + +Gitlab::Seeder.quiet do + projects = Gitlab::Seeder::ProjectRoutes.new + projects.seed! +end diff --git a/db/fixtures/development/04_labels.rb b/db/fixtures/development/04_labels.rb index 21d552c89f5..aff8331a191 100644 --- a/db/fixtures/development/04_labels.rb +++ b/db/fixtures/development/04_labels.rb @@ -37,13 +37,15 @@ class Gitlab::Seeder::ProjectLabels end Gitlab::Seeder.quiet do - puts "\nGenerating group labels" - Group.all.find_each do |group| - Gitlab::Seeder::GroupLabels.new(group).seed! + label_per_group = 10 + puts "\nGenerating group labels: #{Group.not_mass_generated.count * label_per_group}" + Group.not_mass_generated.find_each do |group| + Gitlab::Seeder::GroupLabels.new(group, label_per_group: label_per_group).seed! end - puts "\nGenerating project labels" + label_per_project = 5 + puts "\nGenerating project labels: #{Project.not_mass_generated.count * label_per_project}" Project.not_mass_generated.find_each do |project| - Gitlab::Seeder::ProjectLabels.new(project).seed! + Gitlab::Seeder::ProjectLabels.new(project, label_per_project: label_per_project).seed! end end diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb index 94d3aa59710..7aaaa48d6d4 100644 --- a/db/fixtures/development/06_teams.rb +++ b/db/fixtures/development/06_teams.rb @@ -2,7 +2,7 @@ require './spec/support/sidekiq_middleware' Sidekiq::Testing.inline! do Gitlab::Seeder.quiet do - Group.all.each do |group| + Group.not_mass_generated.each do |group| User.not_mass_generated.sample(4).each do |user| if group.add_user(user, Gitlab::Access.values.sample).persisted? print '.' diff --git a/db/fixtures/development/32_crm.rb b/db/fixtures/development/32_crm.rb index a515af65222..1701763aba0 100644 --- a/db/fixtures/development/32_crm.rb +++ b/db/fixtures/development/32_crm.rb @@ -41,7 +41,7 @@ end Gitlab::Seeder.quiet do puts "\nGenerating group crm organizations and contacts" - Group.where('parent_id IS NULL').first(10).each do |group| + Group.not_mass_generated.where('parent_id IS NULL').first(10).each do |group| Gitlab::Seeder::Crm.new(group).seed! end end diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md index fca80a4a291..3d1401efca4 100644 --- a/doc/user/clusters/agent/install/index.md +++ b/doc/user/clusters/agent/install/index.md @@ -39,6 +39,9 @@ To install the agent in your cluster: You must register an agent with GitLab. +FLAG: +In GitLab 14.10, a [flag](../../../../administration/feature_flags.md) named `certificate_based_clusters` changed the **Actions** menu to focus on the agent rather than certificates. The flag is [enabled on GitLab.com and self-managed](https://gitlab.com/groups/gitlab-org/configure/-/epics/8). + Prerequisites: - For a [GitLab CI/CD workflow](../ci_cd_tunnel.md), ensure that @@ -48,8 +51,7 @@ To register an agent with GitLab: 1. On the top bar, select **Menu > Projects** and find your project. 1. From the left sidebar, select **Infrastructure > Kubernetes clusters**. -1. Select **Actions**. -1. From the **Select an agent** dropdown list: +1. Select **Connect a cluster (agent)**. - If you want to create a configuration with CI/CD defaults, type a name for the agent. - If you already have an [agent configuration file](#create-an-agent-configuration-file), select it from the list. 1. Select **Register an agent**. diff --git a/doc/user/infrastructure/clusters/connect/new_eks_cluster.md b/doc/user/infrastructure/clusters/connect/new_eks_cluster.md index 87b8f510289..50899053cad 100644 --- a/doc/user/infrastructure/clusters/connect/new_eks_cluster.md +++ b/doc/user/infrastructure/clusters/connect/new_eks_cluster.md @@ -48,10 +48,13 @@ This project provides you with: ## Register the agent +FLAG: +In GitLab 14.10, a [flag](../../../../administration/feature_flags.md) named `certificate_based_clusters` changed the **Actions** menu to focus on the agent rather than certificates. The flag is [enabled on GitLab.com and self-managed](https://gitlab.com/groups/gitlab-org/configure/-/epics/8). + To create a GitLab agent for Kubernetes: 1. On the left sidebar, select **Infrastructure > Kubernetes clusters**. -1. Select **Actions**. +1. Select **Connect a cluster (agent)**. 1. From the **Select an agent** dropdown list, select `eks-agent` and select **Register an agent**. 1. GitLab generates a registration token for the agent. Securely store this secret token, as you will need it later. 1. GitLab provides an address for the agent server (KAS), which you will also need later. diff --git a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md index 1ed8b0ef350..f74862e636e 100644 --- a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md +++ b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md @@ -48,10 +48,13 @@ with defaults for name, location, node count, and Kubernetes version. ## Register the agent +FLAG: +In GitLab 14.10, a [flag](../../../../administration/feature_flags.md) named `certificate_based_clusters` changed the **Actions** menu to focus on the agent rather than certificates. The flag is [enabled on GitLab.com and self-managed](https://gitlab.com/groups/gitlab-org/configure/-/epics/8). + To create a GitLab agent for Kubernetes: 1. On the left sidebar, select **Infrastructure > Kubernetes clusters**. -1. Select **Actions**. +1. Select **Connect a cluster (agent)**. 1. From the **Select an agent** dropdown list, select `gke-agent` and select **Register an agent**. 1. GitLab generates a registration token for the agent. Securely store this secret token, as you will need it later. 1. GitLab provides an address for the agent server (KAS), which you will also need later. diff --git a/lib/api/integrations.rb b/lib/api/integrations.rb index ff1d88e35f0..71c55704ddf 100644 --- a/lib/api/integrations.rb +++ b/lib/api/integrations.rb @@ -6,7 +6,7 @@ module API integrations = Helpers::IntegrationsHelpers.integrations integration_classes = Helpers::IntegrationsHelpers.integration_classes - if Rails.env.development? + if Gitlab.dev_or_test_env? integrations['mock-ci'] = [ { required: true, diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index e2df60c46f1..ec514adafc8 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -4,12 +4,24 @@ module Gitlab class Seeder extend ActionView::Helpers::NumberHelper - MASS_INSERT_PROJECT_START = 'mass_insert_project_' - MASS_INSERT_USER_START = 'mass_insert_user_' + MASS_INSERT_PREFIX = 'mass_insert' + MASS_INSERT_PROJECT_START = "#{MASS_INSERT_PREFIX}_project_" + MASS_INSERT_GROUP_START = "#{MASS_INSERT_PREFIX}_group_" + MASS_INSERT_USER_START = "#{MASS_INSERT_PREFIX}_user_" REPORTED_USER_START = 'reported_user_' - ESTIMATED_INSERT_PER_MINUTE = 2_000_000 + ESTIMATED_INSERT_PER_MINUTE = 250_000 MASS_INSERT_ENV = 'MASS_INSERT' + module NamespaceSeed + extend ActiveSupport::Concern + + included do + scope :not_mass_generated, -> do + where.not("path LIKE '#{MASS_INSERT_GROUP_START}%'") + end + end + end + module ProjectSeed extend ActiveSupport::Concern @@ -30,6 +42,10 @@ module Gitlab end end + def self.log_message(message) + puts "#{Time.current}: #{message}" + end + def self.with_mass_insert(size, model) humanized_model_name = model.is_a?(String) ? model : model.model_name.human.pluralize(size) @@ -63,6 +79,7 @@ module Gitlab def self.quiet # Additional seed logic for models. + Namespace.include(NamespaceSeed) Project.include(ProjectSeed) User.include(UserSeed) diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 99ffeb4ec0b..9ac817b9aa0 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -10,7 +10,12 @@ namespace :dev do Gitlab::Database::EachDatabase.each_database_connection do |connection| # Make sure DB statistics are up to date. + # gitlab:setup task can insert quite a bit of data, especially with MASS_INSERT=1 + # so ANALYZE can take more than default 15s statement timeout. This being a dev task, + # we disable the statement timeout for ANALYZE to run and enable it back afterwards. + connection.execute('SET statement_timeout TO 0') connection.execute('ANALYZE') + connection.execute('RESET statement_timeout') end Rake::Task["gitlab:shell:setup"].invoke diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js index 0ca70e0a77e..9632d0f98f4 100644 --- a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js +++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js @@ -31,12 +31,17 @@ describe('unit_format/formatter_factory', () => { expect(formatNumber(12.345, 4)).toBe('12.3450'); }); - it('formats a large integer with a length limit', () => { + it('formats a large integer with a max length - using legacy positional argument', () => { expect(formatNumber(10 ** 7, undefined)).toBe('10,000,000'); expect(formatNumber(10 ** 7, undefined, 9)).toBe('1.00e+7'); expect(formatNumber(10 ** 7, undefined, 10)).toBe('10,000,000'); }); + it('formats a large integer with a max length', () => { + expect(formatNumber(10 ** 7, undefined, { maxLength: 9 })).toBe('1.00e+7'); + expect(formatNumber(10 ** 7, undefined, { maxLength: 10 })).toBe('10,000,000'); + }); + describe('formats with a different locale', () => { let originalLang; @@ -92,7 +97,7 @@ describe('unit_format/formatter_factory', () => { expect(formatSuffix(-1000000)).toBe('-1,000,000pop.'); }); - it('formats a floating point nugative number', () => { + it('formats a floating point negative number', () => { expect(formatSuffix(-0.1)).toBe('-0.1pop.'); expect(formatSuffix(-0.1, 0)).toBe('-0pop.'); expect(formatSuffix(-0.1, 2)).toBe('-0.10pop.'); @@ -108,10 +113,20 @@ describe('unit_format/formatter_factory', () => { expect(formatSuffix(10 ** 10)).toBe('10,000,000,000pop.'); }); - it('formats a large integer with a length limit', () => { + it('formats using a unit separator', () => { + expect(formatSuffix(10, 0, { unitSeparator: ' ' })).toBe('10 pop.'); + expect(formatSuffix(10, 0, { unitSeparator: ' x ' })).toBe('10 x pop.'); + }); + + it('formats a large integer with a max length - using legacy positional argument', () => { expect(formatSuffix(10 ** 7, undefined, 10)).toBe('1.00e+7pop.'); expect(formatSuffix(10 ** 10, undefined, 10)).toBe('1.00e+10pop.'); }); + + it('formats a large integer with a max length', () => { + expect(formatSuffix(10 ** 7, undefined, { maxLength: 10 })).toBe('1.00e+7pop.'); + expect(formatSuffix(10 ** 10, undefined, { maxLength: 10 })).toBe('1.00e+10pop.'); + }); }); describe('scaledSIFormatter', () => { @@ -143,6 +158,10 @@ describe('unit_format/formatter_factory', () => { expect(formatGibibytes(10 ** 10)).toBe('10GB'); expect(formatGibibytes(10 ** 11)).toBe('100GB'); }); + + it('formats bytes using a unit separator', () => { + expect(formatGibibytes(1, 0, { unitSeparator: ' ' })).toBe('1 B'); + }); }); describe('scaled format with offset', () => { @@ -174,6 +193,19 @@ describe('unit_format/formatter_factory', () => { expect(formatGigaBytes(10 ** 9)).toBe('1EB'); }); + it('formats bytes using a unit separator', () => { + expect(formatGigaBytes(1, undefined, { unitSeparator: ' ' })).toBe('1 GB'); + }); + + it('formats long byte numbers with max length - using legacy positional argument', () => { + expect(formatGigaBytes(1, 8, 7)).toBe('1.00e+0GB'); + }); + + it('formats long byte numbers with max length', () => { + expect(formatGigaBytes(1, 8)).toBe('1.00000000GB'); + expect(formatGigaBytes(1, 8, { maxLength: 7 })).toBe('1.00e+0GB'); + }); + it('formatting of too large numbers is not suported', () => { // formatting YB is out of range expect(() => scaledSIFormatter('B', 9)).toThrow(); @@ -216,6 +248,10 @@ describe('unit_format/formatter_factory', () => { expect(formatMilligrams(-100)).toBe('-100mg'); expect(formatMilligrams(-(10 ** 4))).toBe('-10g'); }); + + it('formats using a unit separator', () => { + expect(formatMilligrams(1, undefined, { unitSeparator: ' ' })).toBe('1 mg'); + }); }); }); @@ -253,6 +289,10 @@ describe('unit_format/formatter_factory', () => { expect(formatScaledBin(10 * 1024 ** 3)).toBe('10GiB'); expect(formatScaledBin(100 * 1024 ** 3)).toBe('100GiB'); }); + + it('formats using a unit separator', () => { + expect(formatScaledBin(1, undefined, { unitSeparator: ' ' })).toBe('1 B'); + }); }); describe('scaled format with offset', () => { @@ -288,6 +328,10 @@ describe('unit_format/formatter_factory', () => { expect(formatGibibytes(100 * 1024 ** 3)).toBe('100EiB'); }); + it('formats using a unit separator', () => { + expect(formatGibibytes(1, undefined, { unitSeparator: ' ' })).toBe('1 GiB'); + }); + it('formatting of too large numbers is not suported', () => { // formatting YB is out of range expect(() => scaledBinaryFormatter('B', 9)).toThrow(); diff --git a/spec/frontend/lib/utils/unit_format/index_spec.js b/spec/frontend/lib/utils/unit_format/index_spec.js index 7fd273f1b58..dc9d6ece48e 100644 --- a/spec/frontend/lib/utils/unit_format/index_spec.js +++ b/spec/frontend/lib/utils/unit_format/index_spec.js @@ -74,10 +74,13 @@ describe('unit_format', () => { it('seconds', () => { expect(seconds(1)).toBe('1s'); + expect(seconds(1, undefined, { unitSeparator: ' ' })).toBe('1 s'); }); it('milliseconds', () => { expect(milliseconds(1)).toBe('1ms'); + expect(milliseconds(1, undefined, { unitSeparator: ' ' })).toBe('1 ms'); + expect(milliseconds(100)).toBe('100ms'); expect(milliseconds(1000)).toBe('1,000ms'); expect(milliseconds(10_000)).toBe('10,000ms'); @@ -87,6 +90,7 @@ describe('unit_format', () => { it('decimalBytes', () => { expect(decimalBytes(1)).toBe('1B'); expect(decimalBytes(1, 1)).toBe('1.0B'); + expect(decimalBytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 B'); expect(decimalBytes(10)).toBe('10B'); expect(decimalBytes(10 ** 2)).toBe('100B'); @@ -104,31 +108,37 @@ describe('unit_format', () => { it('kilobytes', () => { expect(kilobytes(1)).toBe('1kB'); expect(kilobytes(1, 1)).toBe('1.0kB'); + expect(kilobytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 kB'); }); it('megabytes', () => { expect(megabytes(1)).toBe('1MB'); expect(megabytes(1, 1)).toBe('1.0MB'); + expect(megabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 MB'); }); it('gigabytes', () => { expect(gigabytes(1)).toBe('1GB'); expect(gigabytes(1, 1)).toBe('1.0GB'); + expect(gigabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 GB'); }); it('terabytes', () => { expect(terabytes(1)).toBe('1TB'); expect(terabytes(1, 1)).toBe('1.0TB'); + expect(terabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 TB'); }); it('petabytes', () => { expect(petabytes(1)).toBe('1PB'); expect(petabytes(1, 1)).toBe('1.0PB'); + expect(petabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 PB'); }); it('bytes', () => { expect(bytes(1)).toBe('1B'); expect(bytes(1, 1)).toBe('1.0B'); + expect(bytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 B'); expect(bytes(10)).toBe('10B'); expect(bytes(100)).toBe('100B'); @@ -142,26 +152,31 @@ describe('unit_format', () => { it('kibibytes', () => { expect(kibibytes(1)).toBe('1KiB'); expect(kibibytes(1, 1)).toBe('1.0KiB'); + expect(kibibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 KiB'); }); it('mebibytes', () => { expect(mebibytes(1)).toBe('1MiB'); expect(mebibytes(1, 1)).toBe('1.0MiB'); + expect(mebibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 MiB'); }); it('gibibytes', () => { expect(gibibytes(1)).toBe('1GiB'); expect(gibibytes(1, 1)).toBe('1.0GiB'); + expect(gibibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 GiB'); }); it('tebibytes', () => { expect(tebibytes(1)).toBe('1TiB'); expect(tebibytes(1, 1)).toBe('1.0TiB'); + expect(tebibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 TiB'); }); it('pebibytes', () => { expect(pebibytes(1)).toBe('1PiB'); expect(pebibytes(1, 1)).toBe('1.0PiB'); + expect(pebibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 PiB'); }); describe('getFormatter', () => { diff --git a/spec/lib/gitlab/seeder_spec.rb b/spec/lib/gitlab/seeder_spec.rb index 71d0a41ef98..a22d47cbfb3 100644 --- a/spec/lib/gitlab/seeder_spec.rb +++ b/spec/lib/gitlab/seeder_spec.rb @@ -3,6 +3,24 @@ require 'spec_helper' RSpec.describe Gitlab::Seeder do + describe Namespace do + subject { described_class } + + it 'has not_mass_generated scope' do + expect { Namespace.not_mass_generated }.to raise_error(NoMethodError) + + Gitlab::Seeder.quiet do + expect { Namespace.not_mass_generated }.not_to raise_error + end + end + + it 'includes NamespaceSeed module' do + Gitlab::Seeder.quiet do + is_expected.to include_module(Gitlab::Seeder::NamespaceSeed) + end + end + end + describe '.quiet' do let(:database_base_models) do { @@ -50,4 +68,13 @@ RSpec.describe Gitlab::Seeder do notification_service.new_note(note) end end + + describe '.log_message' do + it 'prepends timestamp to the logged message' do + freeze_time do + message = "some message." + expect { described_class.log_message(message) }.to output(/#{Time.current}: #{message}/).to_stdout + end + end + end end diff --git a/spec/models/integrations/emails_on_push_spec.rb b/spec/models/integrations/emails_on_push_spec.rb index bdca267f6cb..15aa105e379 100644 --- a/spec/models/integrations/emails_on_push_spec.rb +++ b/spec/models/integrations/emails_on_push_spec.rb @@ -78,9 +78,10 @@ RSpec.describe Integrations::EmailsOnPush do end describe '.valid_recipients' do - let(:recipients) { ' foobar Valid@recipient.com Dup@lica.te dup@lica.te Dup@Lica.te' } + let(:recipients) { ' foobar valid@dup@asd Valid@recipient.com Dup@lica.te dup@lica.te Dup@Lica.te' } it 'removes invalid email addresses and removes duplicates by keeping the original capitalization' do + expect(described_class.valid_recipients(recipients)).not_to contain_exactly('valid@dup@asd') expect(described_class.valid_recipients(recipients)).to contain_exactly('Valid@recipient.com', 'Dup@lica.te') end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3bff45f80dd..622eb0614bb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -726,6 +726,33 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#personal_namespace_holder?' do + let_it_be(:group) { create(:group) } + let_it_be(:namespace_user) { create(:user) } + let_it_be(:admin_user) { create(:user, :admin) } + let_it_be(:personal_project) { create(:project, namespace: namespace_user.namespace) } + let_it_be(:group_project) { create(:project, group: group) } + let_it_be(:another_user) { create(:user) } + let_it_be(:group_owner_user) { create(:user).tap { |user| group.add_owner(user) } } + + where(:project, :user, :result) do + ref(:personal_project) | ref(:namespace_user) | true + ref(:personal_project) | ref(:admin_user) | false + ref(:personal_project) | ref(:another_user) | false + ref(:personal_project) | nil | false + ref(:group_project) | ref(:namespace_user) | false + ref(:group_project) | ref(:group_owner_user) | false + ref(:group_project) | ref(:another_user) | false + ref(:group_project) | nil | false + ref(:group_project) | nil | false + ref(:group_project) | ref(:admin_user) | false + end + + with_them do + it { expect(project.personal_namespace_holder?(user)).to eq(result) } + end + end + describe '#default_pipeline_lock' do let(:project) { build_stubbed(:project) } diff --git a/spec/tasks/dev_rake_spec.rb b/spec/tasks/dev_rake_spec.rb index 7bc27d2732c..ebcea338291 100644 --- a/spec/tasks/dev_rake_spec.rb +++ b/spec/tasks/dev_rake_spec.rb @@ -17,7 +17,9 @@ RSpec.describe 'dev rake tasks' do it 'sets up the development environment', :aggregate_failures do expect(Rake::Task['gitlab:setup']).to receive(:invoke) + expect(connections).to all(receive(:execute).with('SET statement_timeout TO 0')) expect(connections).to all(receive(:execute).with('ANALYZE')) + expect(connections).to all(receive(:execute).with('RESET statement_timeout')) expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)