Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
190128fc72
commit
74d9798736
60 changed files with 856 additions and 178 deletions
|
@ -666,6 +666,7 @@ Gitlab/NamespacedClass:
|
|||
- 'ee/elastic/**/*.rb'
|
||||
- 'scripts/**/*'
|
||||
- 'spec/migrations/**/*.rb'
|
||||
- 'app/experiments/**/*_experiment.rb'
|
||||
|
||||
Lint/HashCompareByIdentity:
|
||||
Enabled: true
|
||||
|
|
|
@ -86,6 +86,7 @@ export const defaultAutocompleteConfig = {
|
|||
labels: true,
|
||||
snippets: true,
|
||||
vulnerabilities: true,
|
||||
contacts: true,
|
||||
};
|
||||
|
||||
class GfmAutoComplete {
|
||||
|
@ -127,6 +128,7 @@ class GfmAutoComplete {
|
|||
if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
|
||||
if (this.enableMap.labels) this.setupLabels($input);
|
||||
if (this.enableMap.snippets) this.setupSnippets($input);
|
||||
if (this.enableMap.contacts) this.setupContacts($input);
|
||||
|
||||
$input.filter('[data-supports-quick-actions="true"]').atwho({
|
||||
at: '/',
|
||||
|
@ -174,9 +176,16 @@ class GfmAutoComplete {
|
|||
let tpl = '/${name} ';
|
||||
let referencePrefix = null;
|
||||
if (value.params.length > 0) {
|
||||
[[referencePrefix]] = value.params;
|
||||
if (/^[@%~]/.test(referencePrefix)) {
|
||||
const regexp = /\[[a-z]+:/;
|
||||
const match = regexp.exec(value.params);
|
||||
if (match) {
|
||||
[referencePrefix] = match;
|
||||
tpl += '<%- referencePrefix %>';
|
||||
} else {
|
||||
[[referencePrefix]] = value.params;
|
||||
if (/^[@%~]/.test(referencePrefix)) {
|
||||
tpl += '<%- referencePrefix %>';
|
||||
}
|
||||
}
|
||||
}
|
||||
return template(tpl, { interpolate: /<%=([\s\S]+?)%>/g })({ referencePrefix });
|
||||
|
@ -619,6 +628,42 @@ class GfmAutoComplete {
|
|||
});
|
||||
}
|
||||
|
||||
setupContacts($input) {
|
||||
$input.atwho({
|
||||
at: '[contact:',
|
||||
suffix: ']',
|
||||
alias: 'contacts',
|
||||
searchKey: 'search',
|
||||
displayTpl(value) {
|
||||
let tmpl = GfmAutoComplete.Loading.template;
|
||||
if (value.email != null) {
|
||||
tmpl = GfmAutoComplete.Contacts.templateFunction(value);
|
||||
}
|
||||
return tmpl;
|
||||
},
|
||||
data: GfmAutoComplete.defaultLoadingData,
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
insertTpl: '${atwho-at}${email}',
|
||||
callbacks: {
|
||||
...this.getDefaultCallbacks(),
|
||||
beforeSave(contacts) {
|
||||
return $.map(contacts, (m) => {
|
||||
if (m.email == null) {
|
||||
return m;
|
||||
}
|
||||
return {
|
||||
id: m.id,
|
||||
email: m.email,
|
||||
firstName: m.first_name,
|
||||
lastName: m.last_name,
|
||||
search: `${m.email}`,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultCallbacks() {
|
||||
const self = this;
|
||||
|
||||
|
@ -790,6 +835,7 @@ GfmAutoComplete.atTypeMap = {
|
|||
'/': 'commands',
|
||||
'[vulnerability:': 'vulnerabilities',
|
||||
$: 'snippets',
|
||||
'[contact:': 'contacts',
|
||||
};
|
||||
|
||||
GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
|
||||
|
@ -883,6 +929,11 @@ GfmAutoComplete.Milestones = {
|
|||
return `<li>${escape(title)}</li>`;
|
||||
},
|
||||
};
|
||||
GfmAutoComplete.Contacts = {
|
||||
templateFunction({ email, firstName, lastName }) {
|
||||
return `<li><small>${firstName} ${lastName}</small> ${escape(email)}</li>`;
|
||||
},
|
||||
};
|
||||
GfmAutoComplete.Loading = {
|
||||
template:
|
||||
'<li style="pointer-events: none;"><span class="spinner align-text-bottom mr-1"></span>Loading...</li>',
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'StepNav',
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
showBackButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showNextButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
nextButtonEnabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot name="before"></slot>
|
||||
<gl-button
|
||||
v-if="showBackButton"
|
||||
category="secondary"
|
||||
data-testid="back-button"
|
||||
@click="$emit('back')"
|
||||
>
|
||||
{{ __('Back') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-if="showNextButton"
|
||||
:disabled="!nextButtonEnabled"
|
||||
category="primary"
|
||||
data-testid="next-button"
|
||||
variant="confirm"
|
||||
@click="$emit('next')"
|
||||
>
|
||||
{{ __('Next') }}
|
||||
</gl-button>
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -9,6 +9,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import { stripHtml } from '~/lib/utils/text_utility';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import MarkdownHeader from './header.vue';
|
||||
import MarkdownToolbar from './toolbar.vue';
|
||||
|
||||
|
@ -23,6 +24,7 @@ export default {
|
|||
GlIcon,
|
||||
Suggestions,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
/**
|
||||
* This prop should be bound to the value of the `<textarea>` element
|
||||
|
@ -217,6 +219,7 @@ export default {
|
|||
labels: this.enableAutocomplete,
|
||||
snippets: this.enableAutocomplete,
|
||||
vulnerabilities: this.enableAutocomplete,
|
||||
contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
|
|
@ -45,6 +45,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:vue_issues_list, project&.group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:contacts_autocomplete, project&.group, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
before_action only: :show do
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class ApplicationExperiment < Gitlab::Experiment
|
||||
def publish(_result = nil)
|
||||
super
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CombinedRegistrationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class CombinedRegistrationExperiment < ApplicationExperiment
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def key_for(source, _ = nil)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EmptyRepoUploadExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class EmptyRepoUploadExperiment < ApplicationExperiment
|
||||
include ProjectCommitCount
|
||||
|
||||
TRACKING_START_DATE = DateTime.parse('2021/4/20')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ForceCompanyTrialExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class ForceCompanyTrialExperiment < ApplicationExperiment
|
||||
exclude :setup_for_personal
|
||||
|
||||
private
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InProductGuidanceEnvironmentsWebideExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class InProductGuidanceEnvironmentsWebideExperiment < ApplicationExperiment
|
||||
exclude :has_environments?
|
||||
|
||||
def control_behavior
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NewProjectSastEnabledExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class NewProjectSastEnabledExperiment < ApplicationExperiment
|
||||
def publish(_result = nil)
|
||||
super
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment
|
||||
exclude :existing_user
|
||||
|
||||
EXPERIMENT_START_DATE = Date.new(2022, 1, 31)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SecurityReportsMrWidgetPromptExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
class SecurityReportsMrWidgetPromptExperiment < ApplicationExperiment
|
||||
def publish(_result = nil)
|
||||
super
|
||||
|
||||
|
|
|
@ -396,7 +396,8 @@ module ApplicationHelper
|
|||
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
milestones: milestones_project_autocomplete_sources_path(object),
|
||||
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
snippets: snippets_project_autocomplete_sources_path(object)
|
||||
snippets: snippets_project_autocomplete_sources_path(object),
|
||||
contacts: contacts_project_autocomplete_sources_path(object)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ class ContainerRepository < ApplicationRecord
|
|||
REQUIRING_CLEANUP_STATUSES = %i[cleanup_unscheduled cleanup_scheduled].freeze
|
||||
IDLE_MIGRATION_STATES = %w[default pre_import_done import_done import_aborted import_skipped].freeze
|
||||
ACTIVE_MIGRATION_STATES = %w[pre_importing importing].freeze
|
||||
ABORTABLE_MIGRATION_STATES = (ACTIVE_MIGRATION_STATES + ['pre_import_done']).freeze
|
||||
MIGRATION_STATES = (IDLE_MIGRATION_STATES + ACTIVE_MIGRATION_STATES).freeze
|
||||
|
||||
belongs_to :project
|
||||
|
@ -17,7 +18,7 @@ class ContainerRepository < ApplicationRecord
|
|||
validates :name, length: { minimum: 0, allow_nil: false }
|
||||
validates :name, uniqueness: { scope: :project_id }
|
||||
validates :migration_state, presence: true, inclusion: { in: MIGRATION_STATES }
|
||||
validates :migration_aborted_in_state, inclusion: { in: ACTIVE_MIGRATION_STATES }, allow_nil: true
|
||||
validates :migration_aborted_in_state, inclusion: { in: ABORTABLE_MIGRATION_STATES }, allow_nil: true
|
||||
|
||||
validates :migration_retries_count, presence: true,
|
||||
numericality: { greater_than_or_equal_to: 0 },
|
||||
|
@ -43,6 +44,9 @@ class ContainerRepository < ApplicationRecord
|
|||
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
|
||||
scope :waiting_for_cleanup, -> { where(expiration_policy_cleanup_status: WAITING_CLEANUP_STATUSES) }
|
||||
scope :expiration_policy_started_at_nil_or_before, ->(timestamp) { where('expiration_policy_started_at < ? OR expiration_policy_started_at IS NULL', timestamp) }
|
||||
scope :with_migration_import_started_at_nil_or_before, ->(timestamp) { where("COALESCE(migration_import_started_at, '01-01-1970') < ?", timestamp) }
|
||||
scope :with_migration_pre_import_started_at_nil_or_before, ->(timestamp) { where("COALESCE(migration_pre_import_started_at, '01-01-1970') < ?", timestamp) }
|
||||
scope :with_migration_pre_import_done_at_nil_or_before, ->(timestamp) { where("COALESCE(migration_pre_import_done_at, '01-01-1970') < ?", timestamp) }
|
||||
scope :with_stale_ongoing_cleanup, ->(threshold) { cleanup_ongoing.where('expiration_policy_started_at < ?', threshold) }
|
||||
|
||||
state_machine :migration_state, initial: :default do
|
||||
|
@ -96,7 +100,7 @@ class ContainerRepository < ApplicationRecord
|
|||
end
|
||||
|
||||
event :abort_import do
|
||||
transition ACTIVE_MIGRATION_STATES.map(&:to_sym) => :import_aborted
|
||||
transition ABORTABLE_MIGRATION_STATES.map(&:to_sym) => :import_aborted
|
||||
end
|
||||
|
||||
event :skip_import do
|
||||
|
@ -181,6 +185,22 @@ class ContainerRepository < ApplicationRecord
|
|||
with_enabled_policy.cleanup_unfinished
|
||||
end
|
||||
|
||||
def self.with_stale_migration(before_timestamp)
|
||||
stale_pre_importing = with_migration_states(:pre_importing)
|
||||
.with_migration_pre_import_started_at_nil_or_before(before_timestamp)
|
||||
stale_pre_import_done = with_migration_states(:pre_import_done)
|
||||
.with_migration_pre_import_done_at_nil_or_before(before_timestamp)
|
||||
stale_importing = with_migration_states(:importing)
|
||||
.with_migration_import_started_at_nil_or_before(before_timestamp)
|
||||
|
||||
union = ::Gitlab::SQL::Union.new([
|
||||
stale_pre_importing,
|
||||
stale_pre_import_done,
|
||||
stale_importing
|
||||
])
|
||||
from("(#{union.to_sql}) #{ContainerRepository.table_name}")
|
||||
end
|
||||
|
||||
def skip_import(reason:)
|
||||
self.migration_skipped_reason = reason
|
||||
|
||||
|
|
|
@ -26,6 +26,18 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
validate :validate_email_format
|
||||
validate :unique_email_for_group_hierarchy
|
||||
|
||||
def self.reference_prefix
|
||||
'[contact:'
|
||||
end
|
||||
|
||||
def self.reference_prefix_quoted
|
||||
'["contact:'
|
||||
end
|
||||
|
||||
def self.reference_postfix
|
||||
']'
|
||||
end
|
||||
|
||||
def self.find_ids_by_emails(group, emails)
|
||||
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
||||
|
||||
|
|
|
@ -149,6 +149,7 @@ class User < ApplicationRecord
|
|||
has_many :members
|
||||
has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, class_name: 'GroupMember'
|
||||
has_many :groups, through: :group_members
|
||||
has_many :groups_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :group_members, source: :group
|
||||
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
|
||||
has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
|
||||
has_many :developer_groups, -> { where(members: { access_level: ::Gitlab::Access::DEVELOPER }) }, through: :group_members, source: :group
|
||||
|
@ -170,6 +171,7 @@ class User < ApplicationRecord
|
|||
has_many :project_members, -> { where(requested_at: nil) }
|
||||
has_many :projects, through: :project_members
|
||||
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
|
||||
has_many :projects_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :project_members, source: :project
|
||||
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :starred_projects, through: :users_star_projects, source: :project
|
||||
has_many :project_authorizations, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
|
|
@ -48,10 +48,16 @@ module Issues
|
|||
end
|
||||
|
||||
def add_by_email
|
||||
contact_ids = ::CustomerRelations::Contact.find_ids_by_emails(project_group, params[:add_emails])
|
||||
contact_ids = ::CustomerRelations::Contact.find_ids_by_emails(project_group, emails(:add_emails))
|
||||
add_by_id(contact_ids)
|
||||
end
|
||||
|
||||
def emails(key)
|
||||
params[key].map do |email|
|
||||
extract_email_from_request_param(email)
|
||||
end
|
||||
end
|
||||
|
||||
def add_by_id(contact_ids)
|
||||
contact_ids -= existing_ids
|
||||
contact_ids.uniq.each do |contact_id|
|
||||
|
@ -69,7 +75,7 @@ module Issues
|
|||
end
|
||||
|
||||
def remove_by_email
|
||||
contact_ids = ::CustomerRelations::IssueContact.find_contact_ids_by_emails(issue.id, params[:remove_emails])
|
||||
contact_ids = ::CustomerRelations::IssueContact.find_contact_ids_by_emails(issue.id, emails(:remove_emails))
|
||||
remove_by_id(contact_ids)
|
||||
end
|
||||
|
||||
|
@ -80,6 +86,13 @@ module Issues
|
|||
.delete_all
|
||||
end
|
||||
|
||||
def extract_email_from_request_param(email_param)
|
||||
email_param.delete_prefix(::CustomerRelations::Contact.reference_prefix_quoted)
|
||||
.delete_prefix(::CustomerRelations::Contact.reference_prefix)
|
||||
.delete_suffix(::CustomerRelations::Contact.reference_postfix)
|
||||
.tr('"', '')
|
||||
end
|
||||
|
||||
def allowed?
|
||||
current_user&.can?(:set_issue_crm_contacts, issue)
|
||||
end
|
||||
|
|
|
@ -13,6 +13,6 @@
|
|||
= _('Report abuse to admin')
|
||||
- if note_editable
|
||||
%li
|
||||
= link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?', qa_selector: 'delete_comment_button' }, remote: true, class: 'js-note-delete' do
|
||||
= link_to note_url(note), method: :delete, data: { confirm: _('Are you sure you want to delete this comment?'), confirm_btn_variant: 'danger', qa_selector: 'delete_comment_button' }, aria: { label: _('Delete comment') }, remote: true, class: 'js-note-delete' do
|
||||
%span.text-danger
|
||||
= _('Delete comment')
|
||||
|
|
|
@ -273,6 +273,15 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: cronjob:container_registry_migration_guard
|
||||
:worker_name: ContainerRegistry::Migration::GuardWorker
|
||||
:feature_category: :container_registry
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:database_batched_background_migration
|
||||
:worker_name: Database::BatchedBackgroundMigrationWorker
|
||||
:feature_category: :database
|
||||
|
|
47
app/workers/container_registry/migration/guard_worker.rb
Normal file
47
app/workers/container_registry/migration/guard_worker.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ContainerRegistry
|
||||
module Migration
|
||||
class GuardWorker
|
||||
include ApplicationWorker
|
||||
# This is a general worker with no context.
|
||||
# It is not scoped to a project, user or group.
|
||||
# We don't have a context.
|
||||
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
|
||||
|
||||
data_consistency :always
|
||||
feature_category :container_registry
|
||||
urgency :low
|
||||
worker_resource_boundary :unknown
|
||||
deduplicate :until_executed
|
||||
idempotent!
|
||||
|
||||
def perform
|
||||
return unless Gitlab.com?
|
||||
|
||||
repositories = ::ContainerRepository.with_stale_migration(step_before_timestamp)
|
||||
.limit(max_capacity)
|
||||
|
||||
# the #to_a is safe as the amount of entries is limited.
|
||||
# In addition, we're calling #each in the next line and we don't want two different SQL queries for these two lines
|
||||
log_extra_metadata_on_done(:stale_migrations_count, repositories.to_a.size)
|
||||
|
||||
repositories.each do |repository|
|
||||
repository.abort_import
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def step_before_timestamp
|
||||
::ContainerRegistry::Migration.max_step_duration.seconds.ago
|
||||
end
|
||||
|
||||
def max_capacity
|
||||
# doubling the actual capacity to prevent issues in case the capacity
|
||||
# is not properly applied
|
||||
::ContainerRegistry::Migration.capacity * 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: contacts_autocomplete
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79639
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352123
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::product planning
|
||||
default_enabled: false
|
|
@ -539,6 +539,9 @@ Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker']['job_class']
|
|||
Settings.cron_jobs['container_expiration_policy_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['container_expiration_policy_worker']['cron'] ||= '50 * * * *'
|
||||
Settings.cron_jobs['container_expiration_policy_worker']['job_class'] = 'ContainerExpirationPolicyWorker'
|
||||
Settings.cron_jobs['container_registry_migration_guard_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['container_registry_migration_guard_worker']['cron'] ||= '*/10 * * * *'
|
||||
Settings.cron_jobs['container_registry_migration_guard_worker']['job_class'] = 'ContainerRegistry::Migration::GuardWorker'
|
||||
Settings.cron_jobs['image_ttl_group_policy_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['image_ttl_group_policy_worker']['cron'] ||= '40 0 * * *'
|
||||
Settings.cron_jobs['image_ttl_group_policy_worker']['job_class'] = 'DependencyProxy::ImageTtlGroupPolicyWorker'
|
||||
|
@ -548,7 +551,6 @@ Settings.cron_jobs['cleanup_dependency_proxy_worker']['job_class'] = 'Dependency
|
|||
Settings.cron_jobs['cleanup_package_registry_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['cleanup_package_registry_worker']['cron'] ||= '20 0,12 * * *'
|
||||
Settings.cron_jobs['cleanup_package_registry_worker']['job_class'] = 'Packages::CleanupPackageRegistryWorker'
|
||||
|
||||
Settings.cron_jobs['x509_issuer_crl_check_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['x509_issuer_crl_check_worker']['cron'] ||= '30 1 * * *'
|
||||
Settings.cron_jobs['x509_issuer_crl_check_worker']['job_class'] = 'X509IssuerCrlCheckWorker'
|
||||
|
|
|
@ -47,6 +47,8 @@
|
|||
- 1
|
||||
- - audit_events_audit_event_streaming
|
||||
- 1
|
||||
- - audit_events_user_impersonation_event_create
|
||||
- 1
|
||||
- - authorized_keys
|
||||
- 2
|
||||
- - authorized_project_update
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddMigrationIndexesToContainerRepositories < Gitlab::Database::Migration[1.0]
|
||||
PRE_IMPORTING_INDEX = 'idx_container_repos_on_pre_import_started_at_when_pre_importing'
|
||||
PRE_IMPORT_DONE_INDEX = 'idx_container_repos_on_pre_import_done_at_when_pre_import_done'
|
||||
IMPORTING_INDEX = 'idx_container_repos_on_import_started_at_when_importing'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :container_repositories, :migration_pre_import_started_at, name: PRE_IMPORTING_INDEX, where: "migration_state = 'pre_importing'"
|
||||
add_concurrent_index :container_repositories, :migration_pre_import_done_at, name: PRE_IMPORT_DONE_INDEX, where: "migration_state = 'pre_import_done'"
|
||||
add_concurrent_index :container_repositories, :migration_import_started_at, name: IMPORTING_INDEX, where: "migration_state = 'importing'"
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :container_repositories, IMPORTING_INDEX
|
||||
remove_concurrent_index_by_name :container_repositories, PRE_IMPORT_DONE_INDEX
|
||||
remove_concurrent_index_by_name :container_repositories, PRE_IMPORTING_INDEX
|
||||
end
|
||||
end
|
1
db/schema_migrations/20220202115350
Normal file
1
db/schema_migrations/20220202115350
Normal file
|
@ -0,0 +1 @@
|
|||
3bcc97592e8e329e39917deffae6619e69215930a688bebad2949f69155b53f9
|
|
@ -25243,6 +25243,12 @@ CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at_enabled ON con
|
|||
|
||||
CREATE INDEX idx_container_repos_on_exp_cleanup_status_project_id_start_date ON container_repositories USING btree (expiration_policy_cleanup_status, project_id, expiration_policy_started_at);
|
||||
|
||||
CREATE INDEX idx_container_repos_on_import_started_at_when_importing ON container_repositories USING btree (migration_import_started_at) WHERE (migration_state = 'importing'::text);
|
||||
|
||||
CREATE INDEX idx_container_repos_on_pre_import_done_at_when_pre_import_done ON container_repositories USING btree (migration_pre_import_done_at) WHERE (migration_state = 'pre_import_done'::text);
|
||||
|
||||
CREATE INDEX idx_container_repos_on_pre_import_started_at_when_pre_importing ON container_repositories USING btree (migration_pre_import_started_at) WHERE (migration_state = 'pre_importing'::text);
|
||||
|
||||
CREATE INDEX idx_deployment_clusters_on_cluster_id_and_kubernetes_namespace ON deployment_clusters USING btree (cluster_id, kubernetes_namespace);
|
||||
|
||||
CREATE INDEX idx_devops_adoption_segments_namespace_end_time ON analytics_devops_adoption_snapshots USING btree (namespace_id, end_time);
|
||||
|
|
|
@ -51,7 +51,10 @@ There are two kinds of events logged:
|
|||
When a user is being [impersonated](../user/admin_area/index.md#user-impersonation), their actions are logged as audit events as usual, with two additional details:
|
||||
|
||||
1. Usual audit events include information about the impersonating administrator. These are visible in their respective Audit Event pages depending on their type (Group/Project/User).
|
||||
1. Extra audit events are recorded for the start and stop of the administrator's impersonation session. These are visible in the instance Audit Events.
|
||||
1. Extra audit events are recorded for the start and stop of the administrator's impersonation session. These are visible in
|
||||
the:
|
||||
- Instance audit events.
|
||||
- Group audit events for all groups the user belongs to (GitLab 14.8 and later). This is limited to 20 groups for performance reasons.
|
||||
|
||||
![audit events](img/impersonated_audit_events_v13_8.png)
|
||||
|
||||
|
@ -103,6 +106,7 @@ From there, you can see the following actions:
|
|||
- Group CI/CD variable added, removed, or protected status changed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30857) in GitLab 13.3.
|
||||
- Compliance framework created, updated, or deleted. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) in GitLab 14.5.
|
||||
- Event streaming destination created, updated, or deleted. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) in GitLab 14.6.
|
||||
- Instance administrator started or stopped impersonation of a group member. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300961) in GitLab 14.8.
|
||||
|
||||
Group events can also be accessed via the [Group Audit Events API](../api/audit_events.md#group-audit-events)
|
||||
|
||||
|
|
|
@ -651,9 +651,11 @@ which variables take precedence.
|
|||
|
||||
The order of precedence for variables is (from highest to lowest):
|
||||
|
||||
1. [Trigger variables](../triggers/index.md#pass-cicd-variables-in-the-api-call),
|
||||
[scheduled pipeline variables](../pipelines/schedules.md#using-variables),
|
||||
and [manual pipeline run variables](#override-a-variable-when-running-a-pipeline-manually).
|
||||
1. These all have the same (highest) precedence:
|
||||
- [Trigger variables](../triggers/index.md#pass-cicd-variables-in-the-api-call).
|
||||
- [Scheduled pipeline variables](../pipelines/schedules.md#using-variables).
|
||||
- [Manual pipeline run variables](#override-a-variable-when-running-a-pipeline-manually).
|
||||
- Variables added when [creating a pipeline with the API](../../api/pipelines.md#create-a-new-pipeline).
|
||||
1. Project [variables](#custom-cicd-variables).
|
||||
1. Group [variables](#add-a-cicd-variable-to-a-group).
|
||||
1. Instance [variables](#add-a-cicd-variable-to-an-instance).
|
||||
|
|
|
@ -232,6 +232,11 @@ v1.0, 2019-01-01
|
|||
|
||||
#### Includes
|
||||
|
||||
NOTE:
|
||||
[Wiki pages](project/wiki/index.md#create-a-new-wiki-page) created with the AsciiDoc
|
||||
format are saved with the file extension `.asciidoc`. When working with AsciiDoc wiki
|
||||
pages, change the file name from `.adoc` to `.asciidoc`.
|
||||
|
||||
```plaintext
|
||||
include::basics.adoc[]
|
||||
|
||||
|
|
|
@ -6,7 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Customer relations management (CRM) **(FREE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `customer_relations`.
|
||||
On GitLab.com, this feature is not available.
|
||||
|
||||
With customer relations management (CRM) you can create a record of contacts
|
||||
(individuals) and organizations (companies) and relate them to issues.
|
||||
|
@ -133,7 +137,7 @@ API.
|
|||
|
||||
### Add contacts to an issue
|
||||
|
||||
To add contacts to an issue use the `/add_contacts`
|
||||
To add contacts to an issue use the `/add_contacts [contact:address@example.com]`
|
||||
[quick action](../project/quick_actions.md).
|
||||
|
||||
You can also add, remove, or replace issue contacts using the
|
||||
|
@ -142,9 +146,25 @@ API.
|
|||
|
||||
### Remove contacts from an issue
|
||||
|
||||
To remove contacts from an issue use the `/remove_contacts`
|
||||
To remove contacts from an issue use the `/remove_contacts [contact:address@example.com]`
|
||||
[quick action](../project/quick_actions.md).
|
||||
|
||||
You can also add, remove, or replace issue contacts using the
|
||||
[GraphQL](../../api/graphql/reference/index.md#mutationissuesetcrmcontacts)
|
||||
API.
|
||||
|
||||
## Autocomplete contacts **(FREE SELF)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `contacts_autocomplete`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `contacts_autocomplete`.
|
||||
On GitLab.com, this feature is not available.
|
||||
This feature is not ready for production use.
|
||||
|
||||
When you use the `/add_contacts` or `/remove_contacts` quick actions, follow them with `[contact:` and an autocomplete list appears:
|
||||
|
||||
```plaintext
|
||||
/add_contacts [contact:
|
||||
/remove_contacts [contact:
|
||||
```
|
||||
|
|
|
@ -539,6 +539,7 @@ GitLab Flavored Markdown recognizes the following:
|
|||
| repository file references | `[README](doc/README.md)` | | |
|
||||
| repository file line references | `[README](doc/README.md#L13)` | | |
|
||||
| [alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` |
|
||||
| contact | `[contact:test@example.com]` | | |
|
||||
|
||||
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222483) in GitLab 13.7.
|
||||
|
||||
|
|
|
@ -402,7 +402,7 @@ To set a WIP limit for a list:
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34723) in GitLab 12.8.
|
||||
> - [View blocking issues when hovering over blocked icon](https://gitlab.com/gitlab-org/gitlab/-/issues/210452) in GitLab 13.10.
|
||||
|
||||
If an issue is blocked by another issue, an icon appears next to its title to indicate its blocked
|
||||
If an issue is [blocked by another issue](issues/related_issues.md#blocking-issues), an icon appears next to its title to indicate its blocked
|
||||
status.
|
||||
|
||||
When you hover over the blocked icon (**{issue-block}**), a detailed information popover is displayed.
|
||||
|
|
|
@ -51,7 +51,7 @@ the issue even if they were actively participating before the change.
|
|||
## Confidential issue indicators
|
||||
|
||||
There are a few things that visually separate a confidential issue from a
|
||||
regular one. In the issues index page view, you can see the eye-slash (**(eye-slash)**) icon
|
||||
regular one. In the issues index page view, you can see the eye-slash (**{eye-slash}**) icon
|
||||
next to the issues that are marked as confidential:
|
||||
|
||||
![Confidential issues index page](img/confidential_issues_index_page.png)
|
||||
|
|
|
@ -15,10 +15,8 @@ value, or complexity a given issue has or costs.
|
|||
|
||||
You can set the weight of an issue during its creation, by changing the
|
||||
value in the dropdown menu. You can set it to a non-negative integer
|
||||
value from 0, 1, 2, and so on. (The database stores a 4-byte value, so the
|
||||
upper bound is essentially limitless.)
|
||||
You can remove weight from an issue
|
||||
as well.
|
||||
value from 0, 1, 2, and so on.
|
||||
You can remove weight from an issue as well.
|
||||
|
||||
This value appears on the right sidebar of an individual issue, as well as
|
||||
in the issues page next to a weight icon (**{weight}**).
|
||||
|
|
|
@ -319,7 +319,7 @@ You can move all open issues from one project to another.
|
|||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Reporter role for the project.
|
||||
- You must have access to the Rails console of the GitLab instance.
|
||||
|
||||
To do it:
|
||||
|
||||
|
@ -510,9 +510,9 @@ Alternatively:
|
|||
|
||||
## Promote an issue to an epic **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3777) in GitLab Ultimate 11.6.
|
||||
> - Moved to GitLab Premium in 12.8.
|
||||
> - Promoting issues to epics via the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233974) in GitLab Premium 13.6.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3777) in GitLab 11.6.
|
||||
> - Moved from GitLab Ultimate to GitLab Premium in 12.8.
|
||||
> - Promoting issues to epics via the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233974) in GitLab 13.6.
|
||||
|
||||
You can promote an issue to an [epic](../../group/epics/index.md) in the immediate parent group.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Multiple Assignees for Issues **(PREMIUM)**
|
||||
|
||||
> - Moved to GitLab Premium in 13.9.
|
||||
> Moved to GitLab Premium in 13.9.
|
||||
|
||||
In large teams, where there is shared ownership of an issue, it can be difficult
|
||||
to track who is working on it, who already completed their contributions, who
|
||||
|
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Linked issues **(FREE)**
|
||||
|
||||
> The simple "relates to" relationship [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212329) to [GitLab Free](https://about.gitlab.com/pricing/) in 13.4.
|
||||
> The simple "relates to" relationship [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212329) from GitLab Premium to GitLab Free in 13.4.
|
||||
|
||||
Linked issues are a bi-directional relationship between any two issues and appear in a block below
|
||||
the issue description. You can link issues in different projects.
|
||||
|
@ -33,8 +33,8 @@ To link one issue to another:
|
|||
select the add linked issue button (**{plus}**).
|
||||
1. Select the relationship between the two issues. Either:
|
||||
- **relates to**
|
||||
- **blocks** **(PREMIUM)**
|
||||
- **is blocked by** **(PREMIUM)**
|
||||
- **[blocks](#blocking-issues)**
|
||||
- **[is blocked by](#blocking-issues)**
|
||||
1. Input the issue number or paste in the full URL of the issue.
|
||||
|
||||
![Adding a related issue](img/related_issues_add_v12_8.png)
|
||||
|
@ -69,3 +69,10 @@ Due to the bi-directional relationship, the relationship no longer appears in ei
|
|||
![Removing a related issue](img/related_issues_remove_v12_8.png)
|
||||
|
||||
Access our [permissions](../../permissions.md) page for more information.
|
||||
|
||||
## Blocking issues **(PREMIUM)**
|
||||
|
||||
When you [add a linked issue](#add-a-linked-issue), you can show that it **blocks** or
|
||||
**is blocked by** another issue.
|
||||
|
||||
Issues that block other issues have an icon (**{issue-block}**) shown in the issue lists and [boards](../issue_board.md).
|
||||
|
|
|
@ -27,7 +27,7 @@ The available sorting options can change based on the context of the list.
|
|||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34247/) in GitLab 13.7.
|
||||
|
||||
When you sort by **Blocking**, the issue list changes to sort descending by the
|
||||
number of issues each issue is blocking.
|
||||
number of issues each issue is [blocking](related_issues.md#blocking-issues).
|
||||
|
||||
## Sorting by created date
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ threads. Some quick actions might not be available to all subscription tiers.
|
|||
|
||||
| Command | Issue | Merge request | Epic | Action |
|
||||
|:-------------------------------------------------------------------------------------------------|:-----------------------|:-----------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `/add_contacts email1 email2` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add one or more [CRM contacts](../crm/index.md) ([introduced in GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73413)). |
|
||||
| `/add_contacts [contact:email1@example.com] [contact:email2@example.com]` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add one or more [CRM contacts](../crm/index.md) ([introduced in GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73413)). |
|
||||
| `/approve` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Approve the merge request. |
|
||||
| `/assign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Assign one or more users. |
|
||||
| `/assign me` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Assign yourself. |
|
||||
|
@ -89,7 +89,7 @@ threads. Some quick actions might not be available to all subscription tiers.
|
|||
| `/relabel ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Replace current labels with those specified. |
|
||||
| `/relate #issue1 #issue2` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Mark issues as related. |
|
||||
| `/remove_child_epic <epic>` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes | Remove child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/-/issues/7330)). |
|
||||
| `/remove_contacts email1 email2` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove one or more [CRM contacts](../crm/index.md) ([introduced in GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73413)). |
|
||||
| `/remove_contacts [contact:email1@example.com] [contact:email2@example.com]` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove one or more [CRM contacts](../crm/index.md) ([introduced in GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73413)). |
|
||||
| `/remove_due_date` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove due date. |
|
||||
| `/remove_epic` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove from epic. |
|
||||
| `/remove_estimate` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove time estimate. |
|
||||
|
|
|
@ -158,7 +158,7 @@ These shortcuts are available when using a [filtered search input](search/index.
|
|||
| <kbd>⌘</kbd> (Mac) + <kbd>⌫</kbd> | Clear entire search filter. |
|
||||
| <kbd>⌥</kbd> (Mac) / <kbd>Control</kbd> + <kbd>⌫</kbd> | Clear one token at a time. |
|
||||
|
||||
## Epics **(ULTIMATE)**
|
||||
## Epics **(PREMIUM)**
|
||||
|
||||
These shortcuts are available when viewing [epics](group/epics/index.md):
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ module ContainerRegistry
|
|||
|
||||
def faraday(timeout_enabled: true)
|
||||
@faraday ||= faraday_base(timeout_enabled: timeout_enabled) do |conn|
|
||||
initialize_connection(conn, @options, &method(:accept_manifest))
|
||||
initialize_connection(conn, @options, &method(:configure_connection))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -105,7 +105,7 @@ module ContainerRegistry
|
|||
faraday_redirect.get(uri)
|
||||
end
|
||||
|
||||
def accept_manifest(conn)
|
||||
def configure_connection(conn)
|
||||
conn.headers['Accept'] = ACCEPTED_TYPES
|
||||
|
||||
conn.response :json, content_type: 'application/json'
|
||||
|
|
|
@ -4,6 +4,8 @@ module ContainerRegistry
|
|||
class GitlabApiClient < BaseClient
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
JSON_TYPE = 'application/json'
|
||||
|
||||
IMPORT_RESPONSES = {
|
||||
200 => :already_imported,
|
||||
202 => :ok,
|
||||
|
@ -46,9 +48,20 @@ module ContainerRegistry
|
|||
private
|
||||
|
||||
def start_import_for(path, pre:)
|
||||
faraday.put("/gitlab/v1/import/#{path}") do |req|
|
||||
faraday.put(import_url_for(path)) do |req|
|
||||
req.params['pre'] = pre.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def import_url_for(path)
|
||||
"/gitlab/v1/import/#{path}/"
|
||||
end
|
||||
|
||||
# overrides the default configuration
|
||||
def configure_connection(conn)
|
||||
conn.headers['Accept'] = [JSON_TYPE]
|
||||
|
||||
conn.response :json, content_type: JSON_TYPE
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ContainerRegistry
|
||||
class Migration
|
||||
module Migration
|
||||
class << self
|
||||
delegate :container_registry_import_max_tags_count, to: ::Gitlab::CurrentSettings
|
||||
delegate :container_registry_import_max_retries, to: ::Gitlab::CurrentSettings
|
||||
|
|
|
@ -19,7 +19,7 @@ module Gitlab
|
|||
|
||||
relations = [
|
||||
# The project a user has direct access to.
|
||||
user.projects.select_for_project_authorization,
|
||||
user.projects_with_active_memberships.select_for_project_authorization,
|
||||
|
||||
# The personal projects of the user.
|
||||
user.personal_projects.select_as_maintainer_for_project_authorization,
|
||||
|
@ -65,7 +65,7 @@ module Gitlab
|
|||
group_group_links = GroupGroupLink.arel_table
|
||||
|
||||
# Namespaces the user is a member of.
|
||||
cte << user.groups
|
||||
cte << user.groups_with_active_memberships
|
||||
.select([namespaces[:id], members[:access_level]])
|
||||
.except(:order)
|
||||
|
||||
|
@ -99,6 +99,7 @@ module Gitlab
|
|||
.and(members[:source_type].eq('Namespace'))
|
||||
.and(members[:requested_at].eq(nil))
|
||||
.and(members[:user_id].eq(user.id))
|
||||
.and(members[:state].eq(::Member::STATE_ACTIVE))
|
||||
.and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS))
|
||||
|
||||
Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond))
|
||||
|
@ -120,6 +121,7 @@ module Gitlab
|
|||
.and(members[:source_type].eq('Namespace'))
|
||||
.and(members[:requested_at].eq(nil))
|
||||
.and(members[:user_id].eq(user.id))
|
||||
.and(members[:state].eq(::Member::STATE_ACTIVE))
|
||||
.and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS))
|
||||
Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond))
|
||||
end
|
||||
|
|
|
@ -287,7 +287,7 @@ module Gitlab
|
|||
|
||||
desc _('Add customer relation contacts')
|
||||
explanation _('Add customer relation contact(s).')
|
||||
params 'contact@example.com person@example.org'
|
||||
params '[contact:contact@example.com] [contact:person@example.org]'
|
||||
types Issue
|
||||
condition do
|
||||
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
|
||||
|
@ -302,7 +302,7 @@ module Gitlab
|
|||
|
||||
desc _('Remove customer relation contacts')
|
||||
explanation _('Remove customer relation contact(s).')
|
||||
params 'contact@example.com person@example.org'
|
||||
params '[contact:contact@example.com] [contact:person@example.org]'
|
||||
types Issue
|
||||
condition do
|
||||
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
|
||||
|
|
|
@ -4690,6 +4690,9 @@ msgstr ""
|
|||
msgid "Are you sure you want to delete this SSH key?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete this comment?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete this deploy key?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34916,6 +34919,21 @@ msgstr ""
|
|||
msgid "Subscriptions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscriptions|Chat with sales"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscriptions|Close"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscriptions|Not ready to buy yet?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscriptions|Start a free trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscriptions|We understand. Maybe you have some questions for our sales team, or maybe you'd like to try some of the paid features first. What would you like to do?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscription|Your subscription for %{strong}%{namespace_name}%{strong_close} has expired and you are now on %{pricing_link_start}the GitLab Free tier%{pricing_link_end}. Don't worry, your data is safe. Get in touch with our support team (%{support_email}). They'll gladly help with your subscription renewal."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -35,6 +35,18 @@ FactoryBot.define do
|
|||
access_level { GroupMember::MINIMAL_ACCESS }
|
||||
end
|
||||
|
||||
trait :awaiting do
|
||||
after(:create) do |member|
|
||||
member.update!(state: ::Member::STATE_AWAITING)
|
||||
end
|
||||
end
|
||||
|
||||
trait :active do
|
||||
after(:create) do |member|
|
||||
member.update!(state: ::Member::STATE_ACTIVE)
|
||||
end
|
||||
end
|
||||
|
||||
transient do
|
||||
tasks_to_be_done { [] }
|
||||
end
|
||||
|
|
|
@ -24,6 +24,18 @@ FactoryBot.define do
|
|||
after(:build) { |project_member, _| project_member.user.block! }
|
||||
end
|
||||
|
||||
trait :awaiting do
|
||||
after(:create) do |member|
|
||||
member.update!(state: ::Member::STATE_AWAITING)
|
||||
end
|
||||
end
|
||||
|
||||
trait :active do
|
||||
after(:create) do |member|
|
||||
member.update!(state: ::Member::STATE_ACTIVE)
|
||||
end
|
||||
end
|
||||
|
||||
transient do
|
||||
tasks_to_be_done { [] }
|
||||
end
|
||||
|
|
|
@ -6,7 +6,8 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
|
||||
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
|
||||
let_it_be(:label) { create(:label, project: project, title: 'special+') }
|
||||
let_it_be(:label_scoped) { create(:label, project: project, title: 'scoped::label') }
|
||||
|
@ -19,9 +20,9 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
let_it_be(:label_xss) { create(:label, project: project, title: label_xss_title) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(user)
|
||||
project.add_maintainer(user_xss)
|
||||
project.add_maintainer(user2)
|
||||
group.add_maintainer(user)
|
||||
group.add_maintainer(user_xss)
|
||||
group.add_maintainer(user2)
|
||||
end
|
||||
|
||||
describe 'new issue page' do
|
||||
|
@ -381,6 +382,30 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'contact' do
|
||||
let_it_be(:contacts) { create_list(:contact, 2, group: group) }
|
||||
|
||||
before do
|
||||
fill_in 'Comment', with: '/add_contacts [contact:'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'shows contacts list in the autocomplete menu' do
|
||||
page.within(find_autocomplete_menu) do
|
||||
expect(page).to have_selector('li', count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows all contacts' do
|
||||
page.within(find_autocomplete_menu) do
|
||||
expected_data = contacts.map { |c| "#{c.first_name} #{c.last_name} #{c.email}"}
|
||||
|
||||
expect(page.all('li').map(&:text)).to match_array(expected_data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -7,43 +7,10 @@
|
|||
"title": { "type": "string" },
|
||||
"description": { "type": ["string", "null"] },
|
||||
"state": { "type": "string" },
|
||||
"merged_by": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"id": { "type": "integer" },
|
||||
"state": { "type": "string" },
|
||||
"avatar_url": { "type": "uri" },
|
||||
"web_url": { "type": "uri" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"merge_user": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"id": { "type": "integer" },
|
||||
"state": { "type": "string" },
|
||||
"avatar_url": { "type": "uri" },
|
||||
"web_url": { "type": "uri" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"merged_by": { "$ref": "user/basic.json" },
|
||||
"merge_user": { "$ref": "user/basic.json" },
|
||||
"merged_at": { "type": ["string", "null"] },
|
||||
"closed_by": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"id": { "type": "integer" },
|
||||
"state": { "type": "string" },
|
||||
"avatar_url": { "type": "uri" },
|
||||
"web_url": { "type": "uri" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"closed_by": { "$ref": "user/basic.json" },
|
||||
"closed_at": { "type": ["string", "null"], "format": "date-time" },
|
||||
"created_at": { "type": "string", "format": "date-time" },
|
||||
"updated_at": { "type": "string", "format": "date-time" },
|
||||
|
@ -51,36 +18,20 @@
|
|||
"source_branch": { "type": "string" },
|
||||
"upvotes": { "type": "integer" },
|
||||
"downvotes": { "type": "integer" },
|
||||
"author": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"id": { "type": "integer" },
|
||||
"state": { "type": "string" },
|
||||
"avatar_url": { "type": "uri" },
|
||||
"web_url": { "type": "uri" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"assignee": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"id": { "type": "integer" },
|
||||
"state": { "type": "string" },
|
||||
"avatar_url": { "type": "uri" },
|
||||
"web_url": { "type": "uri" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"author": { "$ref": "user/basic.json" },
|
||||
"assignee": { "$ref": "user/basic.json" },
|
||||
"assignees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "user/basic.json"
|
||||
}
|
||||
},
|
||||
"reviewers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "user/basic.json"
|
||||
}
|
||||
},
|
||||
"source_project_id": { "type": "integer" },
|
||||
"target_project_id": { "type": "integer" },
|
||||
"labels": {
|
||||
|
|
79
spec/frontend/pipeline_wizard/components/step_nav_spec.js
Normal file
79
spec/frontend/pipeline_wizard/components/step_nav_spec.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import StepNav from '~/pipeline_wizard/components/step_nav.vue';
|
||||
|
||||
describe('Pipeline Wizard - Step Navigation Component', () => {
|
||||
const defaultProps = { showBackButton: true, showNextButton: true };
|
||||
|
||||
let wrapper;
|
||||
let prevButton;
|
||||
let nextButton;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = mountExtended(StepNav, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
prevButton = wrapper.findByTestId('back-button');
|
||||
nextButton = wrapper.findByTestId('next-button');
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it.each`
|
||||
scenario | showBackButton | showNextButton
|
||||
${'does not show prev button'} | ${false} | ${false}
|
||||
${'has prev, but not next'} | ${true} | ${false}
|
||||
${'has next, but not prev'} | ${false} | ${true}
|
||||
${'has both next and prev'} | ${true} | ${true}
|
||||
`('$scenario', async ({ showBackButton, showNextButton }) => {
|
||||
createComponent({ showBackButton, showNextButton });
|
||||
|
||||
expect(prevButton.exists()).toBe(showBackButton);
|
||||
expect(nextButton.exists()).toBe(showNextButton);
|
||||
});
|
||||
|
||||
it('shows the expected button text', () => {
|
||||
createComponent();
|
||||
|
||||
expect(prevButton.text()).toBe('Back');
|
||||
expect(nextButton.text()).toBe('Next');
|
||||
});
|
||||
|
||||
it('emits "back" events when clicking prev button', async () => {
|
||||
createComponent();
|
||||
|
||||
await prevButton.trigger('click');
|
||||
expect(wrapper.emitted().back.length).toBe(1);
|
||||
});
|
||||
|
||||
it('emits "next" events when clicking next button', async () => {
|
||||
createComponent();
|
||||
|
||||
await nextButton.trigger('click');
|
||||
expect(wrapper.emitted().next.length).toBe(1);
|
||||
});
|
||||
|
||||
it('enables the next button if nextButtonEnabled ist set to true', async () => {
|
||||
createComponent({ nextButtonEnabled: true });
|
||||
|
||||
expect(nextButton.attributes('disabled')).not.toBe('disabled');
|
||||
});
|
||||
|
||||
it('disables the next button if nextButtonEnabled ist set to false', async () => {
|
||||
createComponent({ nextButtonEnabled: false });
|
||||
|
||||
expect(nextButton.attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('does not emit "next" event when clicking next button while nextButtonEnabled ist set to false', async () => {
|
||||
createComponent({ nextButtonEnabled: false });
|
||||
|
||||
await nextButton.trigger('click');
|
||||
|
||||
expect(wrapper.emitted().next).toBe(undefined);
|
||||
});
|
||||
});
|
|
@ -63,6 +63,11 @@ describe('Markdown field component', () => {
|
|||
textareaValue,
|
||||
lines,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
contactsAutocomplete: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -289,7 +289,7 @@ RSpec.describe ApplicationHelper do
|
|||
|
||||
it 'returns paths for autocomplete_sources_controller' do
|
||||
sources = helper.autocomplete_data_sources(project, noteable_type)
|
||||
expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets])
|
||||
expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :contacts])
|
||||
sources.keys.each do |key|
|
||||
expect(sources[key]).not_to be_nil
|
||||
end
|
||||
|
|
|
@ -7,6 +7,8 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
|
|||
|
||||
include_context 'container registry client'
|
||||
|
||||
let(:path) { 'namespace/path/to/repository' }
|
||||
|
||||
describe '#supports_gitlab_api?' do
|
||||
subject { client.supports_gitlab_api? }
|
||||
|
||||
|
@ -30,9 +32,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
|
|||
|
||||
it 'returns the expected result' do
|
||||
if expect_registry_to_be_pinged
|
||||
expect_next_instance_of(Faraday::Connection) do |connection|
|
||||
expect(connection).to receive(:run_request).and_call_original
|
||||
end
|
||||
expect(Faraday::Connection).to receive(:new).and_call_original
|
||||
else
|
||||
expect(Faraday::Connection).not_to receive(:new)
|
||||
end
|
||||
|
@ -54,9 +54,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
|
|||
end
|
||||
|
||||
describe '#pre_import_repository' do
|
||||
let(:path) { 'namespace/path/to/repository' }
|
||||
|
||||
subject { client.pre_import_repository('namespace/path/to/repository') }
|
||||
subject { client.pre_import_repository(path) }
|
||||
|
||||
where(:status_code, :expected_result) do
|
||||
200 | :already_imported
|
||||
|
@ -80,9 +78,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
|
|||
end
|
||||
|
||||
describe '#pre_import_repository' do
|
||||
let(:path) { 'namespace/path/to/repository' }
|
||||
|
||||
subject { client.import_repository('namespace/path/to/repository') }
|
||||
subject { client.import_repository(path) }
|
||||
|
||||
where(:status_code, :expected_result) do
|
||||
200 | :already_imported
|
||||
|
@ -129,9 +125,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
|
|||
|
||||
it 'returns the expected result' do
|
||||
if expect_registry_to_be_pinged
|
||||
expect_next_instance_of(Faraday::Connection) do |connection|
|
||||
expect(connection).to receive(:run_request).and_call_original
|
||||
end
|
||||
expect(Faraday::Connection).to receive(:new).and_call_original
|
||||
else
|
||||
expect(Faraday::Connection).not_to receive(:new)
|
||||
end
|
||||
|
@ -166,13 +160,15 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
|
|||
end
|
||||
|
||||
def stub_pre_import(path, status_code, pre:)
|
||||
stub_request(:put, "#{registry_api_url}/gitlab/v1/import/#{path}?pre=#{pre}")
|
||||
stub_request(:put, "#{registry_api_url}/gitlab/v1/import/#{path}/?pre=#{pre}")
|
||||
.with(headers: { 'Accept' => described_class::JSON_TYPE })
|
||||
.to_return(status: status_code, body: '')
|
||||
end
|
||||
|
||||
def stub_registry_gitlab_api_support(supported = true)
|
||||
status_code = supported ? 200 : 404
|
||||
stub_request(:get, "#{registry_api_url}/gitlab/v1/")
|
||||
.with(headers: { 'Accept' => described_class::JSON_TYPE })
|
||||
.to_return(status: status_code, body: '')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -345,4 +345,76 @@ RSpec.describe Gitlab::ProjectAuthorizations do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pending memberships' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject(:mapping) { map_access_levels(authorizations) }
|
||||
|
||||
context 'group membership' do
|
||||
let!(:group_project) { create(:project, namespace: group) }
|
||||
|
||||
before do
|
||||
create(:group_member, :developer, :awaiting, user: user, group: group)
|
||||
end
|
||||
|
||||
it 'does not create authorization' do
|
||||
expect(mapping[group_project.id]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'inherited group membership' do
|
||||
let!(:sub_group) { create(:group, parent: group) }
|
||||
let!(:sub_group_project) { create(:project, namespace: sub_group) }
|
||||
|
||||
before do
|
||||
create(:group_member, :developer, :awaiting, user: user, group: group)
|
||||
end
|
||||
|
||||
it 'does not create authorization' do
|
||||
expect(mapping[sub_group_project.id]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'project membership' do
|
||||
let!(:group_project) { create(:project, namespace: group) }
|
||||
|
||||
before do
|
||||
create(:project_member, :developer, :awaiting, user: user, project: group_project)
|
||||
end
|
||||
|
||||
it 'does not create authorization' do
|
||||
expect(mapping[group_project.id]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'shared group' do
|
||||
let!(:shared_group) { create(:group) }
|
||||
let!(:shared_group_project) { create(:project, namespace: shared_group) }
|
||||
|
||||
before do
|
||||
create(:group_group_link, shared_group: shared_group, shared_with_group: group)
|
||||
create(:group_member, :developer, :awaiting, user: user, group: group)
|
||||
end
|
||||
|
||||
it 'does not create authorization' do
|
||||
expect(mapping[shared_group_project.id]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'shared project' do
|
||||
let!(:another_group) { create(:group) }
|
||||
let!(:shared_project) { create(:project, namespace: another_group) }
|
||||
|
||||
before do
|
||||
create(:project_group_link, group: group, project: shared_project)
|
||||
create(:group_member, :developer, :awaiting, user: user, group: group)
|
||||
end
|
||||
|
||||
it 'does not create authorization' do
|
||||
expect(mapping[shared_project.id]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,12 +37,12 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
it { is_expected.to validate_presence_of(:migration_retries_count) }
|
||||
it { is_expected.to validate_numericality_of(:migration_retries_count).is_greater_than_or_equal_to(0) }
|
||||
|
||||
it { is_expected.to validate_inclusion_of(:migration_aborted_in_state).in_array(ContainerRepository::ACTIVE_MIGRATION_STATES) }
|
||||
it { is_expected.to validate_inclusion_of(:migration_aborted_in_state).in_array(described_class::ABORTABLE_MIGRATION_STATES) }
|
||||
it { is_expected.to allow_value(nil).for(:migration_aborted_in_state) }
|
||||
|
||||
context 'migration_state' do
|
||||
it { is_expected.to validate_presence_of(:migration_state) }
|
||||
it { is_expected.to validate_inclusion_of(:migration_state).in_array(ContainerRepository::MIGRATION_STATES) }
|
||||
it { is_expected.to validate_inclusion_of(:migration_state).in_array(described_class::MIGRATION_STATES) }
|
||||
|
||||
describe 'pre_importing' do
|
||||
it 'validates expected attributes' do
|
||||
|
@ -161,7 +161,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
end
|
||||
|
||||
shared_examples 'transitioning from allowed states' do |allowed_states|
|
||||
ContainerRepository::MIGRATION_STATES.each do |state|
|
||||
described_class::MIGRATION_STATES.each do |state|
|
||||
result = allowed_states.include?(state)
|
||||
|
||||
context "when transitioning from #{state}" do
|
||||
|
@ -283,7 +283,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
|
||||
subject { repository.abort_import }
|
||||
|
||||
it_behaves_like 'transitioning from allowed states', %w[pre_importing importing]
|
||||
it_behaves_like 'transitioning from allowed states', %w[pre_importing pre_import_done importing]
|
||||
|
||||
it 'sets migration_aborted_at and migration_aborted_at and increments the retry count' do
|
||||
expect { subject }.to change { repository.migration_aborted_at }
|
||||
|
@ -634,7 +634,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
let(:path) { ContainerRegistry::Path.new(project.full_path + '/some/image') }
|
||||
|
||||
it 'does not throw validation errors and only creates one repository' do
|
||||
expect { repository_creation_race(path) }.to change { ContainerRepository.count }.by(1)
|
||||
expect { repository_creation_race(path) }.to change { described_class.count }.by(1)
|
||||
end
|
||||
|
||||
it 'retrieves a persisted repository for all concurrent calls' do
|
||||
|
@ -652,7 +652,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
Thread.new do
|
||||
true while wait_for_it
|
||||
|
||||
::ContainerRepository.find_or_create_from_path(path)
|
||||
described_class.find_or_create_from_path(path)
|
||||
end
|
||||
end
|
||||
wait_for_it = false
|
||||
|
@ -788,6 +788,36 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
it { is_expected.to contain_exactly(repository1, repository2, repository4) }
|
||||
end
|
||||
|
||||
describe '.with_migration_import_started_at_nil_or_before' do
|
||||
let_it_be(:repository1) { create(:container_repository, migration_import_started_at: 5.minutes.ago) }
|
||||
let_it_be(:repository2) { create(:container_repository, migration_import_started_at: nil) }
|
||||
let_it_be(:repository3) { create(:container_repository, migration_import_started_at: 10.minutes.ago) }
|
||||
|
||||
subject { described_class.with_migration_import_started_at_nil_or_before(7.minutes.ago) }
|
||||
|
||||
it { is_expected.to contain_exactly(repository2, repository3) }
|
||||
end
|
||||
|
||||
describe '.with_migration_pre_import_started_at_nil_or_before' do
|
||||
let_it_be(:repository1) { create(:container_repository, migration_pre_import_started_at: 5.minutes.ago) }
|
||||
let_it_be(:repository2) { create(:container_repository, migration_pre_import_started_at: nil) }
|
||||
let_it_be(:repository3) { create(:container_repository, migration_pre_import_started_at: 10.minutes.ago) }
|
||||
|
||||
subject { described_class.with_migration_pre_import_started_at_nil_or_before(7.minutes.ago) }
|
||||
|
||||
it { is_expected.to contain_exactly(repository2, repository3) }
|
||||
end
|
||||
|
||||
describe '.with_migration_pre_import_done_at_nil_or_before' do
|
||||
let_it_be(:repository1) { create(:container_repository, migration_pre_import_done_at: 5.minutes.ago) }
|
||||
let_it_be(:repository2) { create(:container_repository, migration_pre_import_done_at: nil) }
|
||||
let_it_be(:repository3) { create(:container_repository, migration_pre_import_done_at: 10.minutes.ago) }
|
||||
|
||||
subject { described_class.with_migration_pre_import_done_at_nil_or_before(7.minutes.ago) }
|
||||
|
||||
it { is_expected.to contain_exactly(repository2, repository3) }
|
||||
end
|
||||
|
||||
describe '.with_stale_ongoing_cleanup' do
|
||||
let_it_be(:repository1) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.day.ago) }
|
||||
let_it_be(:repository2) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 25.minutes.ago) }
|
||||
|
@ -837,7 +867,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
describe '#migration_in_active_state?' do
|
||||
subject { container_repository.migration_in_active_state? }
|
||||
|
||||
ContainerRepository::MIGRATION_STATES.each do |state|
|
||||
described_class::MIGRATION_STATES.each do |state|
|
||||
context "when in #{state} migration_state" do
|
||||
let(:container_repository) { create(:container_repository, state.to_sym)}
|
||||
|
||||
|
@ -849,7 +879,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
describe '#migration_importing?' do
|
||||
subject { container_repository.migration_importing? }
|
||||
|
||||
ContainerRepository::MIGRATION_STATES.each do |state|
|
||||
described_class::MIGRATION_STATES.each do |state|
|
||||
context "when in #{state} migration_state" do
|
||||
let(:container_repository) { create(:container_repository, state.to_sym)}
|
||||
|
||||
|
@ -861,7 +891,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
describe '#migration_pre_importing?' do
|
||||
subject { container_repository.migration_pre_importing? }
|
||||
|
||||
ContainerRepository::MIGRATION_STATES.each do |state|
|
||||
described_class::MIGRATION_STATES.each do |state|
|
||||
context "when in #{state} migration_state" do
|
||||
let(:container_repository) { create(:container_repository, state.to_sym)}
|
||||
|
||||
|
@ -922,4 +952,34 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_stale_migration' do
|
||||
let_it_be(:repository) { create(:container_repository) }
|
||||
let_it_be(:stale_pre_importing_old_timestamp) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 10.minutes.ago) }
|
||||
let_it_be(:stale_pre_importing_nil_timestamp) { create(:container_repository, :pre_importing).tap { |r| r.update_column(:migration_pre_import_started_at, nil) } }
|
||||
let_it_be(:stale_pre_importing_recent_timestamp) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 2.minutes.ago) }
|
||||
|
||||
let_it_be(:stale_pre_import_done_old_timestamp) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 10.minutes.ago) }
|
||||
let_it_be(:stale_pre_import_done_nil_timestamp) { create(:container_repository, :pre_import_done).tap { |r| r.update_column(:migration_pre_import_done_at, nil) } }
|
||||
let_it_be(:stale_pre_import_done_recent_timestamp) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 2.minutes.ago) }
|
||||
|
||||
let_it_be(:stale_importing_old_timestamp) { create(:container_repository, :importing, migration_import_started_at: 10.minutes.ago) }
|
||||
let_it_be(:stale_importing_nil_timestamp) { create(:container_repository, :importing).tap { |r| r.update_column(:migration_import_started_at, nil) } }
|
||||
let_it_be(:stale_importing_recent_timestamp) { create(:container_repository, :importing, migration_import_started_at: 2.minutes.ago) }
|
||||
|
||||
let(:stale_migrations) do
|
||||
[
|
||||
stale_pre_importing_old_timestamp,
|
||||
stale_pre_importing_nil_timestamp,
|
||||
stale_pre_import_done_old_timestamp,
|
||||
stale_pre_import_done_nil_timestamp,
|
||||
stale_importing_old_timestamp,
|
||||
stale_importing_nil_timestamp
|
||||
]
|
||||
end
|
||||
|
||||
subject { described_class.with_stale_migration(5.minutes.ago) }
|
||||
|
||||
it { is_expected.to contain_exactly(*stale_migrations) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,18 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
|
||||
end
|
||||
|
||||
describe '.reference_prefix' do
|
||||
it { expect(described_class.reference_prefix).to eq('[contact:') }
|
||||
end
|
||||
|
||||
describe '.reference_prefix_quoted' do
|
||||
it { expect(described_class.reference_prefix_quoted).to eq('["contact:') }
|
||||
end
|
||||
|
||||
describe '.reference_postfix' do
|
||||
it { expect(described_class.reference_postfix).to eq(']') }
|
||||
end
|
||||
|
||||
describe '#unique_email_for_group_hierarchy' do
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:group) { create(:group, parent: parent) }
|
||||
|
|
|
@ -7,20 +7,31 @@ RSpec.describe Issues::SetCrmContactsService do
|
|||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
|
||||
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
|
||||
|
||||
before do
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
|
||||
let_it_be(:issue, reload: true) { create(:issue, project: project) }
|
||||
let_it_be(:issue_contact_1) do
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[0]).contact
|
||||
end
|
||||
|
||||
let_it_be(:issue_contact_2) do
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[1]).contact
|
||||
end
|
||||
|
||||
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
|
||||
|
||||
subject(:set_crm_contacts) do
|
||||
described_class.new(project: project, current_user: user, params: params).execute(issue)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
shared_examples 'setting contacts' do
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array(expected_contacts)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has no permission' do
|
||||
let(:params) { { replace_ids: [contacts[1].id, contacts[2].id] } }
|
||||
|
||||
|
@ -67,56 +78,56 @@ RSpec.describe Issues::SetCrmContactsService do
|
|||
|
||||
context 'replace' do
|
||||
let(:params) { { replace_ids: [contacts[1].id, contacts[2].id] } }
|
||||
let(:expected_contacts) { [contacts[1], contacts[2]] }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[1], contacts[2]])
|
||||
end
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
|
||||
context 'add' do
|
||||
let(:params) { { add_ids: [contacts[3].id] } }
|
||||
let(:added_contact) { contacts[3] }
|
||||
let(:params) { { add_ids: [added_contact.id] } }
|
||||
let(:expected_contacts) { [issue_contact_1, issue_contact_2, added_contact] }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[1], contacts[3]])
|
||||
end
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
|
||||
context 'add by email' do
|
||||
let(:params) { { add_emails: [contacts[3].email] } }
|
||||
let(:added_contact) { contacts[3] }
|
||||
let(:expected_contacts) { [issue_contact_1, issue_contact_2, added_contact] }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
context 'with pure emails in params' do
|
||||
let(:params) { { add_emails: [contacts[3].email] } }
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[1], contacts[3]])
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
|
||||
context 'with autocomplete prefix emails in params' do
|
||||
let(:params) { { add_emails: ["[\"contact:\"#{contacts[3].email}\"]"] } }
|
||||
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
end
|
||||
|
||||
context 'remove' do
|
||||
let(:params) { { remove_ids: [contacts[0].id] } }
|
||||
let(:expected_contacts) { [contacts[1]] }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[1]])
|
||||
end
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
|
||||
context 'remove by email' do
|
||||
let(:params) { { remove_emails: [contacts[0].email] } }
|
||||
let(:expected_contacts) { [contacts[1]] }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
context 'with pure email in params' do
|
||||
let(:params) { { remove_emails: [contacts[0].email] } }
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[1]])
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
|
||||
context 'with autocomplete prefix and suffix email in params' do
|
||||
let(:params) { { remove_emails: ["[contact:#{contacts[0].email}]"] } }
|
||||
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -145,15 +156,19 @@ RSpec.describe Issues::SetCrmContactsService do
|
|||
|
||||
context 'when combining params' do
|
||||
let(:error_invalid_params) { 'You cannot combine replace_ids with add_ids or remove_ids' }
|
||||
let(:expected_contacts) { [contacts[0], contacts[3]] }
|
||||
|
||||
context 'add and remove' do
|
||||
let(:params) { { remove_ids: [contacts[1].id], add_ids: [contacts[3].id] } }
|
||||
context 'with contact ids' do
|
||||
let(:params) { { remove_ids: [contacts[1].id], add_ids: [contacts[3].id] } }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[3]])
|
||||
context 'with contact emails' do
|
||||
let(:params) { { remove_emails: [contacts[1].email], add_emails: ["[\"contact:#{contacts[3].email}]"] } }
|
||||
|
||||
it_behaves_like 'setting contacts'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
115
spec/workers/container_registry/migration/guard_worker_spec.rb
Normal file
115
spec/workers/container_registry/migration/guard_worker_spec.rb
Normal file
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
|
||||
include_context 'container registry client'
|
||||
|
||||
let(:worker) { described_class.new }
|
||||
|
||||
describe '#perform' do
|
||||
let(:pre_importing_migrations) { ::ContainerRepository.with_migration_states(:pre_importing) }
|
||||
let(:pre_import_done_migrations) { ::ContainerRepository.with_migration_states(:pre_import_done) }
|
||||
let(:importing_migrations) { ::ContainerRepository.with_migration_states(:importing) }
|
||||
let(:import_aborted_migrations) { ::ContainerRepository.with_migration_states(:import_aborted) }
|
||||
let(:import_done_migrations) { ::ContainerRepository.with_migration_states(:import_done) }
|
||||
|
||||
subject { worker.perform }
|
||||
|
||||
before do
|
||||
stub_container_registry_config(enabled: true, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key')
|
||||
allow(::ContainerRegistry::Migration).to receive(:max_step_duration).and_return(5.minutes)
|
||||
end
|
||||
|
||||
context 'on gitlab.com' do
|
||||
before do
|
||||
allow(::Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
context 'with no stale migrations' do
|
||||
it_behaves_like 'an idempotent worker'
|
||||
|
||||
it 'will not update any migration state' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 0)
|
||||
expect { subject }
|
||||
.to not_change(pre_importing_migrations, :count)
|
||||
.and not_change(pre_import_done_migrations, :count)
|
||||
.and not_change(importing_migrations, :count)
|
||||
.and not_change(import_aborted_migrations, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pre_importing stale migrations' do
|
||||
let(:ongoing_migration) { create(:container_repository, :pre_importing) }
|
||||
let(:stale_migration) { create(:container_repository, :pre_importing, migration_pre_import_started_at: 10.minutes.ago) }
|
||||
|
||||
it 'will abort the migration' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
expect { subject }
|
||||
.to change(pre_importing_migrations, :count).by(-1)
|
||||
.and not_change(pre_import_done_migrations, :count)
|
||||
.and not_change(importing_migrations, :count)
|
||||
.and not_change(import_done_migrations, :count)
|
||||
.and change(import_aborted_migrations, :count).by(1)
|
||||
.and change { stale_migration.reload.migration_state }.from('pre_importing').to('import_aborted')
|
||||
.and not_change { ongoing_migration.migration_state }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pre_import_done stale migrations' do
|
||||
let(:ongoing_migration) { create(:container_repository, :pre_import_done) }
|
||||
let(:stale_migration) { create(:container_repository, :pre_import_done, migration_pre_import_done_at: 10.minutes.ago) }
|
||||
|
||||
before do
|
||||
allow(::ContainerRegistry::Migration).to receive(:max_step_duration).and_return(5.minutes)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
end
|
||||
|
||||
it 'will abort the migration' do
|
||||
expect { subject }
|
||||
.to not_change(pre_importing_migrations, :count)
|
||||
.and change(pre_import_done_migrations, :count).by(-1)
|
||||
.and not_change(importing_migrations, :count)
|
||||
.and not_change(import_done_migrations, :count)
|
||||
.and change(import_aborted_migrations, :count).by(1)
|
||||
.and change { stale_migration.reload.migration_state }.from('pre_import_done').to('import_aborted')
|
||||
.and not_change { ongoing_migration.migration_state }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with importing stale migrations' do
|
||||
let(:ongoing_migration) { create(:container_repository, :importing) }
|
||||
let(:stale_migration) { create(:container_repository, :importing, migration_import_started_at: 10.minutes.ago) }
|
||||
|
||||
before do
|
||||
allow(::ContainerRegistry::Migration).to receive(:max_step_duration).and_return(5.minutes)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_migrations_count, 1)
|
||||
end
|
||||
|
||||
it 'will abort the migration' do
|
||||
expect { subject }
|
||||
.to not_change(pre_importing_migrations, :count)
|
||||
.and not_change(pre_import_done_migrations, :count)
|
||||
.and change(importing_migrations, :count).by(-1)
|
||||
.and not_change(import_done_migrations, :count)
|
||||
.and change(import_aborted_migrations, :count).by(1)
|
||||
.and change { stale_migration.reload.migration_state }.from('importing').to('import_aborted')
|
||||
.and not_change { ongoing_migration.migration_state }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'not on gitlab.com' do
|
||||
before do
|
||||
allow(::Gitlab).to receive(:com?).and_return(false)
|
||||
end
|
||||
|
||||
it 'is a no op' do
|
||||
expect(::ContainerRepository).not_to receive(:with_stale_migration)
|
||||
expect(worker).not_to receive(:log_extra_metadata_on_done)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue