Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2022-03-29 00:09:12 +00:00
parent fc8a3b9422
commit 2c99b3e0f3
28 changed files with 619 additions and 116 deletions

View File

@ -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}`;
};
};

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 '.'

View File

@ -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

View File

@ -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**.

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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();

View File

@ -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', () => {

View File

@ -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

View File

@ -78,9 +78,10 @@ RSpec.describe Integrations::EmailsOnPush do
end
describe '.valid_recipients' do
let(:recipients) { '<invalid> foobar Valid@recipient.com Dup@lica.te dup@lica.te Dup@Lica.te' }
let(:recipients) { '<invalid> 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

View File

@ -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) }

View File

@ -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)