Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-22 15:07:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-22 15:07:28 +0300
commit4220cf46a314ac1c4d88be13608752bc07bb28fb (patch)
tree169db13fd06c7622bbcf59b707d57d516f44e299
parent4c7e34071eceb05a9ce271354c21de7487e4ff84 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/style/open_struct_use.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/admin/topics/components/remove_avatar.vue13
-rw-r--r--app/assets/javascripts/admin/topics/index.js3
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js6
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js28
-rw-r--r--app/assets/javascripts/pages/profiles/preferences/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js54
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js8
-rw-r--r--app/helpers/issues_helper.rb6
-rw-r--r--app/models/customer_relations/contact.rb32
-rw-r--r--app/models/customer_relations/issue_contact.rb16
-rw-r--r--app/models/customer_relations/organization.rb28
-rw-r--r--app/models/group.rb5
-rw-r--r--app/models/namespace.rb5
-rw-r--r--app/services/concerns/incident_management/usage_data.rb5
-rw-r--r--app/services/groups/transfer_service.rb33
-rw-r--r--app/services/issuable_links/create_service.rb14
-rw-r--r--app/services/issuable_links/destroy_service.rb6
-rw-r--r--app/services/issue_links/create_service.rb2
-rw-r--r--app/services/issue_links/destroy_service.rb2
-rw-r--r--app/services/notes/create_service.rb2
-rw-r--r--app/views/admin/topics/_form.html.haml2
-rw-r--r--app/views/projects/issues/_form.html.haml2
-rw-r--r--config/feature_flags/development/block_namespace_serialization.yml8
-rw-r--r--db/fixtures/development/32_crm.rb2
-rw-r--r--db/migrate/20220314184009_create_protected_environment_approval_rules.rb30
-rw-r--r--db/migrate/20220314184109_add_user_fk_to_protected_environment_approval_rules.rb15
-rw-r--r--db/migrate/20220314184209_add_group_fk_to_protected_environment_approval_rules.rb15
-rw-r--r--db/migrate/20220314194009_add_approval_rule_id_to_deployment_approvals.rb7
-rw-r--r--db/migrate/20220314204009_add_approval_rule_fk_to_deployment_approvals.rb20
-rw-r--r--db/post_migrate/20220316112118_update_organizations_name_index_add_id.rb20
-rw-r--r--db/post_migrate/20220316112206_add_contacts_index_on_group_email_and_id.rb15
-rw-r--r--db/schema_migrations/202203141840091
-rw-r--r--db/schema_migrations/202203141841091
-rw-r--r--db/schema_migrations/202203141842091
-rw-r--r--db/schema_migrations/202203141940091
-rw-r--r--db/schema_migrations/202203142040091
-rw-r--r--db/schema_migrations/202203161121181
-rw-r--r--db/schema_migrations/202203161122061
-rw-r--r--db/structure.sql52
-rw-r--r--doc/administration/integration/kroki.md2
-rw-r--r--doc/api/linked_epics.md117
-rw-r--r--doc/api/users.md22
-rw-r--r--doc/development/service_ping/index.md3
-rw-r--r--lib/api/entities/user_with_admin.rb1
-rw-r--r--lib/api/users.rb7
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--lib/gitlab/import_export/fast_hash_serializer.rb2
-rw-r--r--lib/gitlab/import_export/json/streaming_serializer.rb2
-rw-r--r--lib/mattermost/session.rb7
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/deprecation_toolkit_env.rb3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/admin.json3
-rw-r--r--spec/frontend/admin/topics/components/remove_avatar_spec.js11
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js44
-rw-r--r--spec/helpers/issues_helper_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/mattermost/session_spec.rb6
-rw-r--r--spec/models/customer_relations/contact_spec.rb40
-rw-r--r--spec/models/customer_relations/issue_contact_spec.rb12
-rw-r--r--spec/models/customer_relations/organization_spec.rb28
-rw-r--r--spec/models/integration_spec.rb50
-rw-r--r--spec/models/namespace_spec.rb14
-rw-r--r--spec/requests/api/users_spec.rb24
-rw-r--r--spec/services/groups/transfer_service_spec.rb105
-rw-r--r--spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb6
69 files changed, 889 insertions, 125 deletions
diff --git a/.rubocop_todo/style/open_struct_use.yml b/.rubocop_todo/style/open_struct_use.yml
index c459ea9d49c..e22c59fd251 100644
--- a/.rubocop_todo/style/open_struct_use.yml
+++ b/.rubocop_todo/style/open_struct_use.yml
@@ -11,7 +11,6 @@ Style/OpenStructUse:
- lib/gitlab/git/diff_collection.rb
- lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
- lib/gitlab/testing/request_inspector_middleware.rb
- - lib/mattermost/session.rb
- spec/factories/go_module_versions.rb
- spec/factories/wiki_pages.rb
- spec/graphql/mutations/branches/create_spec.rb
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index e015c905aa3..ccc4d935619 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-f21e9469e94600f50ecb01b98d46f54dd7b33b5c
+719c5a5bd2b5ddb54de519d6873ccb1636f7b450
diff --git a/app/assets/javascripts/admin/topics/components/remove_avatar.vue b/app/assets/javascripts/admin/topics/components/remove_avatar.vue
index 5e94d6185e0..a54c30a8336 100644
--- a/app/assets/javascripts/admin/topics/components/remove_avatar.vue
+++ b/app/assets/javascripts/admin/topics/components/remove_avatar.vue
@@ -1,6 +1,6 @@
<script>
import { uniqueId } from 'lodash';
-import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
@@ -8,11 +8,12 @@ export default {
components: {
GlButton,
GlModal,
+ GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
- inject: ['path'],
+ inject: ['path', 'name'],
data() {
return {
modalId: uniqueId('remove-topic-avatar-'),
@@ -25,8 +26,8 @@ export default {
},
i18n: {
remove: __('Remove avatar'),
- title: __('Confirm remove avatar'),
- body: __('Avatar will be removed. Are you sure?'),
+ title: __('Remove topic avatar'),
+ body: __('Topic avatar for %{name} will be removed. This cannot be undone.'),
},
modal: {
actionPrimary: {
@@ -57,7 +58,9 @@ export default {
:modal-id="modalId"
size="sm"
@primary="deleteApplication"
- >{{ $options.i18n.body }}
+ ><gl-sprintf :message="$options.i18n.body"
+ ><template #name>{{ name }}</template></gl-sprintf
+ >
<form ref="deleteForm" method="post" :action="path">
<input type="hidden" name="_method" value="delete" />
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
diff --git a/app/assets/javascripts/admin/topics/index.js b/app/assets/javascripts/admin/topics/index.js
index 8fbcadf3369..09e9b20f220 100644
--- a/app/assets/javascripts/admin/topics/index.js
+++ b/app/assets/javascripts/admin/topics/index.js
@@ -8,12 +8,13 @@ export default () => {
return false;
}
- const { path } = el.dataset;
+ const { path, name } = el.dataset;
return new Vue({
el,
provide: {
path,
+ name,
},
render(h) {
return h(RemoveAvatar);
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index 88c1748db0b..018cadad50f 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -12,6 +12,7 @@ import ZenMode from '~/zen_mode';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+const DATA_ISSUES_NEW_PATH = 'data-new-issue-path';
function organizeQuery(obj, isFallbackKey = false) {
if (!obj[MR_SOURCE_BRANCH] && !obj[MR_TARGET_BRANCH]) {
@@ -68,6 +69,7 @@ export default class IssuableForm {
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
this.zenMode = new ZenMode();
+ this.newIssuePath = form[0].getAttribute(DATA_ISSUES_NEW_PATH);
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
if (!(this.titleField.length && this.descriptionField.length)) {
@@ -104,8 +106,8 @@ export default class IssuableForm {
}
initAutosave() {
- const { search } = document.location;
- const searchTerm = format(search);
+ const { search, pathname } = document.location;
+ const searchTerm = this.newIssuePath === pathname ? '' : format(search);
const fallbackKey = getFallbackKey();
this.autosave = new Autosave(
diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js
index 320f0f8aa6c..39800ec5067 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/index.js
@@ -48,4 +48,4 @@ export function initJiraConnect() {
});
}
-document.addEventListener('DOMContentLoaded', initJiraConnect);
+initJiraConnect();
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
index a99e0dfa4f0..a1ba920b322 100644
--- a/app/assets/javascripts/pages/groups/clusters/index/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -1,8 +1,6 @@
import initClustersListApp from '~/clusters_list';
import PersistentUserCallout from '~/persistent_user_callout';
-document.addEventListener('DOMContentLoaded', () => {
- const callout = document.querySelector('.gcp-signup-offer');
- PersistentUserCallout.factory(callout);
- initClustersListApp();
-});
+const callout = document.querySelector('.gcp-signup-offer');
+PersistentUserCallout.factory(callout);
+initClustersListApp();
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 96487e14e30..58ca195d7b9 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -10,21 +10,19 @@ import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import initConfirmDanger from '~/init_confirm_danger';
-document.addEventListener('DOMContentLoaded', () => {
- initFilePickers();
- initConfirmDanger();
- initSettingsPanels();
- initTransferGroupForm();
- dirtySubmitFactory(
- document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
- );
- mountBadgeSettings(GROUP_BADGE);
+initFilePickers();
+initConfirmDanger();
+initSettingsPanels();
+initTransferGroupForm();
+dirtySubmitFactory(
+ document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
+);
+mountBadgeSettings(GROUP_BADGE);
- // Initialize Subgroups selector
- groupsSelect();
+// Initialize Subgroups selector
+groupsSelect();
- projectSelect();
+projectSelect();
- initSearchSettings();
- initCascadingSettingsLockPopovers();
-});
+initSearchSettings();
+initCascadingSettingsLockPopovers();
diff --git a/app/assets/javascripts/pages/profiles/preferences/show/index.js b/app/assets/javascripts/pages/profiles/preferences/show/index.js
index d489ed80f46..2922ff88721 100644
--- a/app/assets/javascripts/pages/profiles/preferences/show/index.js
+++ b/app/assets/javascripts/pages/profiles/preferences/show/index.js
@@ -1,3 +1,3 @@
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
-document.addEventListener('DOMContentLoaded', initProfilePreferences);
+initProfilePreferences();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index e88dbf20e1b..43ab829f5f9 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -10,36 +10,34 @@ import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_to
import initSettingsPanels from '~/settings_panels';
import { initTokenAccess } from '~/token_access';
-document.addEventListener('DOMContentLoaded', () => {
- // Initialize expandable settings panels
- initSettingsPanels();
+// Initialize expandable settings panels
+initSettingsPanels();
- const runnerToken = document.querySelector('.js-secret-runner-token');
- if (runnerToken) {
- const runnerTokenSecretValue = new SecretValues({
- container: runnerToken,
- });
- runnerTokenSecretValue.init();
- }
-
- initVariableList();
-
- // hide extra auto devops settings based checkbox state
- const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
- const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
- document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => {
- const { target } = event;
- if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
- autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
+const runnerToken = document.querySelector('.js-secret-runner-token');
+if (runnerToken) {
+ const runnerTokenSecretValue = new SecretValues({
+ container: runnerToken,
});
+ runnerTokenSecretValue.init();
+}
- registrySettingsApp();
- initDeployFreeze();
+initVariableList();
- initSettingsPipelinesTriggers();
- initArtifactsSettings();
- initSharedRunnersToggle();
- initInstallRunner();
- initRunnerAwsDeployments();
- initTokenAccess();
+// hide extra auto devops settings based checkbox state
+const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
+const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
+document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => {
+ const { target } = event;
+ if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
+ autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
});
+
+registrySettingsApp();
+initDeployFreeze();
+
+initSettingsPipelinesTriggers();
+initArtifactsSettings();
+initSharedRunnersToggle();
+initInstallRunner();
+initRunnerAwsDeployments();
+initTokenAccess();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index e90954c14c5..d45052d76f4 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,9 +1,7 @@
import MirrorRepos from '~/mirrors/mirror_repos';
import initForm from '../form';
-document.addEventListener('DOMContentLoaded', () => {
- initForm();
+initForm();
- const mirrorReposContainer = document.querySelector('.js-mirror-settings');
- if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
-});
+const mirrorReposContainer = document.querySelector('.js-mirror-settings');
+if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 298162fe970..b3b8be3da8e 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -239,6 +239,12 @@ module IssuesHelper
)
end
+ def issues_form_data(project)
+ {
+ new_issue_path: new_project_issue_path(project)
+ }
+ end
+
# Overridden in EE
def scoped_labels_available?(parent)
false
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index 4fa2c3fb8cf..cdb449e00bf 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -23,7 +23,7 @@ class CustomerRelations::Contact < ApplicationRecord
validates :last_name, presence: true, length: { maximum: 255 }
validates :email, length: { maximum: 255 }
validates :description, length: { maximum: 1024 }
- validates :email, uniqueness: { scope: :group_id }
+ validates :email, uniqueness: { case_sensitive: false, scope: :group_id }
validate :validate_email_format
validate :validate_root_group
@@ -42,7 +42,7 @@ class CustomerRelations::Contact < ApplicationRecord
def self.find_ids_by_emails(group, emails)
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
- where(group: group, email: emails).pluck(:id)
+ where(group: group).where('lower(email) in (?)', emails.map(&:downcase)).pluck(:id)
end
def self.exists_for_group?(group)
@@ -51,6 +51,34 @@ class CustomerRelations::Contact < ApplicationRecord
exists?(group: group)
end
+ def self.move_to_root_group(group)
+ update_query = <<~SQL
+ UPDATE #{CustomerRelations::IssueContact.table_name}
+ SET contact_id = new_contacts.id
+ FROM #{table_name} AS existing_contacts
+ JOIN #{table_name} AS new_contacts ON new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
+ WHERE existing_contacts.group_id = :new_group_id AND contact_id = existing_contacts.id
+ SQL
+ connection.execute(sanitize_sql([
+ update_query,
+ old_group_id: group.root_ancestor.id,
+ new_group_id: group.id
+ ]))
+
+ dupes_query = <<~SQL
+ DELETE FROM #{table_name} AS existing_contacts
+ USING #{table_name} AS new_contacts
+ WHERE existing_contacts.group_id = :new_group_id AND new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
+ SQL
+ connection.execute(sanitize_sql([
+ dupes_query,
+ old_group_id: group.root_ancestor.id,
+ new_group_id: group.id
+ ]))
+
+ where(group: group).update_all(group_id: group.root_ancestor.id)
+ end
+
private
def validate_email_format
diff --git a/app/models/customer_relations/issue_contact.rb b/app/models/customer_relations/issue_contact.rb
index dc7a3fd87bc..70a30e583d5 100644
--- a/app/models/customer_relations/issue_contact.rb
+++ b/app/models/customer_relations/issue_contact.rb
@@ -8,6 +8,8 @@ class CustomerRelations::IssueContact < ApplicationRecord
validate :contact_belongs_to_root_group
+ BATCH_DELETE_SIZE = 1_000
+
def self.find_contact_ids_by_emails(issue_id, emails)
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
@@ -17,9 +19,17 @@ class CustomerRelations::IssueContact < ApplicationRecord
end
def self.delete_for_project(project_id)
- joins(:issue)
- .where(issues: { project_id: project_id })
- .delete_all
+ loop do
+ deleted_records = joins(:issue).where(issues: { project_id: project_id }).limit(BATCH_DELETE_SIZE).delete_all
+ break if deleted_records == 0
+ end
+ end
+
+ def self.delete_for_group(group)
+ loop do
+ deleted_records = joins(issue: :project).where(projects: { namespace: group.self_and_descendants }).limit(BATCH_DELETE_SIZE).delete_all
+ break if deleted_records == 0
+ end
end
private
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index a23b9d8fe28..32adcc7492b 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -26,6 +26,34 @@ class CustomerRelations::Organization < ApplicationRecord
.where('LOWER(name) = LOWER(?)', name)
end
+ def self.move_to_root_group(group)
+ update_query = <<~SQL
+ UPDATE #{CustomerRelations::Contact.table_name}
+ SET organization_id = new_organizations.id
+ FROM #{table_name} AS existing_organizations
+ JOIN #{table_name} AS new_organizations ON new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
+ WHERE existing_organizations.group_id = :new_group_id AND organization_id = existing_organizations.id
+ SQL
+ connection.execute(sanitize_sql([
+ update_query,
+ old_group_id: group.root_ancestor.id,
+ new_group_id: group.id
+ ]))
+
+ dupes_query = <<~SQL
+ DELETE FROM #{table_name} AS existing_organizations
+ USING #{table_name} AS new_organizations
+ WHERE existing_organizations.group_id = :new_group_id AND new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
+ SQL
+ connection.execute(sanitize_sql([
+ dupes_query,
+ old_group_id: group.root_ancestor.id,
+ new_group_id: group.id
+ ]))
+
+ where(group: group).update_all(group_id: group.root_ancestor.id)
+ end
+
private
def validate_root_group
diff --git a/app/models/group.rb b/app/models/group.rb
index d05a202e013..acf4c165f9a 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -61,8 +61,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
- has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group
- has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group
+ # AR defaults to nullify when trying to delete via has_many associations unless we set dependent: :delete_all
+ has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index ffaeb2071f6..873298ab9ba 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -15,6 +15,7 @@ class Namespace < ApplicationRecord
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
include EachBatch
+ include BlocksUnsafeSerialization
# Temporary column used for back-filling project namespaces.
# Remove it once the back-filling of all project namespaces is done.
@@ -660,6 +661,10 @@ class Namespace < ApplicationRecord
# Use SHA2 of `traversal_ids` to account for moving a namespace within the same root ancestor hierarchy.
"namespaces:{#{traversal_ids.first}}:first_auto_devops_config:#{group_id}:#{Digest::SHA2.hexdigest(traversal_ids.join(' '))}"
end
+
+ def allow_serialization?(options = nil)
+ Feature.disabled?(:block_namespace_serialization, self, default_enabled: :yaml) || super
+ end
end
Namespace.prepend_mod_with('Namespace')
diff --git a/app/services/concerns/incident_management/usage_data.rb b/app/services/concerns/incident_management/usage_data.rb
index b91aa59099d..27e60029ea3 100644
--- a/app/services/concerns/incident_management/usage_data.rb
+++ b/app/services/concerns/incident_management/usage_data.rb
@@ -9,10 +9,5 @@ module IncidentManagement
track_usage_event(:"incident_management_#{action}", current_user.id)
end
-
- # No-op as optionally overridden in implementing classes.
- # For use to provide checks before calling #track_incident_action.
- def track_event
- end
end
end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 10ff4961faf..f2e959396bc 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -25,10 +25,15 @@ module Groups
private
def proceed_to_transfer
+ old_root_ancestor_id = @group.root_ancestor.id
+ was_root_group = @group.root?
+
Group.transaction do
update_group_attributes
ensure_ownership
update_integrations
+ remove_issue_contacts(old_root_ancestor_id, was_root_group)
+ update_crm_objects(was_root_group)
end
post_update_hooks(@updated_project_ids)
@@ -53,6 +58,17 @@ module Groups
raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images?
raise_transfer_error(:cannot_transfer_to_subgroup) if transfer_to_subgroup?
raise_transfer_error(:group_contains_npm_packages) if group_with_npm_packages?
+ raise_transfer_error(:no_permissions_to_migrate_crm) if no_permissions_to_migrate_crm?
+ end
+
+ def no_permissions_to_migrate_crm?
+ return false unless group && @new_parent_group
+ return false if group.root_ancestor == @new_parent_group.root_ancestor
+
+ return true if group.contacts.exists? && !current_user.can?(:admin_crm_contact, @new_parent_group.root_ancestor)
+ return true if group.organizations.exists? && !current_user.can?(:admin_crm_organization, @new_parent_group.root_ancestor)
+
+ false
end
def group_with_npm_packages?
@@ -202,7 +218,8 @@ module Groups
invalid_policies: s_("TransferGroup|You don't have enough permissions."),
group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.'),
cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.'),
- group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.')
+ group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.'),
+ no_permissions_to_migrate_crm: s_("TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
}.freeze
end
@@ -238,6 +255,20 @@ module Groups
namespace_id: group.id
}
end
+
+ def update_crm_objects(was_root_group)
+ return unless was_root_group
+
+ CustomerRelations::Contact.move_to_root_group(group)
+ CustomerRelations::Organization.move_to_root_group(group)
+ end
+
+ def remove_issue_contacts(old_root_ancestor_id, was_root_group)
+ return if was_root_group
+ return if old_root_ancestor_id == @group.root_ancestor.id
+
+ CustomerRelations::IssueContact.delete_for_group(@group)
+ end
end
end
diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb
index 802260c8fae..44f26ba52dc 100644
--- a/app/services/issuable_links/create_service.rb
+++ b/app/services/issuable_links/create_service.rb
@@ -2,8 +2,6 @@
module IssuableLinks
class CreateService < BaseService
- include IncidentManagement::UsageData
-
attr_reader :issuable, :current_user, :params
def initialize(issuable, user, params)
@@ -25,7 +23,7 @@ module IssuableLinks
end
@errors = []
- create_links
+ references = create_links
if @errors.present?
return error(@errors.join('. '), 422)
@@ -33,7 +31,7 @@ module IssuableLinks
track_event
- success
+ success(created_references: references)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -66,7 +64,7 @@ module IssuableLinks
end
def link_issuables(target_issuables)
- target_issuables.each do |referenced_object|
+ target_issuables.map do |referenced_object|
link = relate_issuables(referenced_object)
unless link.valid?
@@ -75,6 +73,8 @@ module IssuableLinks
error: link.errors.messages.values.flatten.to_sentence
}
end
+
+ link
end
end
@@ -142,6 +142,10 @@ module IssuableLinks
def set_link_type(_link)
# no-op
end
+
+ def track_event
+ # no-op
+ end
end
end
diff --git a/app/services/issuable_links/destroy_service.rb b/app/services/issuable_links/destroy_service.rb
index 19edd008b0a..204cf7ce966 100644
--- a/app/services/issuable_links/destroy_service.rb
+++ b/app/services/issuable_links/destroy_service.rb
@@ -2,8 +2,6 @@
module IssuableLinks
class DestroyService < BaseService
- include IncidentManagement::UsageData
-
attr_reader :link, :current_user, :source, :target
def initialize(link, user)
@@ -41,5 +39,9 @@ module IssuableLinks
def not_found_message
'No Issue Link found'
end
+
+ def track_event
+ # no op
+ end
end
end
diff --git a/app/services/issue_links/create_service.rb b/app/services/issue_links/create_service.rb
index 1c6621ce0a1..7f509f3b3e0 100644
--- a/app/services/issue_links/create_service.rb
+++ b/app/services/issue_links/create_service.rb
@@ -2,6 +2,8 @@
module IssueLinks
class CreateService < IssuableLinks::CreateService
+ include IncidentManagement::UsageData
+
def linkable_issuables(issues)
@linkable_issuables ||= begin
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
diff --git a/app/services/issue_links/destroy_service.rb b/app/services/issue_links/destroy_service.rb
index e2422ecaca9..9116e9fb703 100644
--- a/app/services/issue_links/destroy_service.rb
+++ b/app/services/issue_links/destroy_service.rb
@@ -2,6 +2,8 @@
module IssueLinks
class DestroyService < IssuableLinks::DestroyService
+ include IncidentManagement::UsageData
+
private
def permission_to_remove_relation?
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 9a0db3bb9aa..d32d1c8ca12 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -111,7 +111,7 @@ module Notes
def track_event(note, user)
track_note_creation_usage_for_issues(note) if note.for_issue?
track_note_creation_usage_for_merge_requests(note) if note.for_merge_request?
- track_usage_event(:incident_management_incident_comment, user.id) if note.for_issue? && note.noteable.incident?
+ track_incident_action(user, note.noteable, 'incident_comment') if note.for_issue?
if Feature.enabled?(:notes_create_service_tracking, project)
Gitlab::Tracking.event('Notes::CreateService', 'execute', **tracking_data_for(note))
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
index c40484ea494..50ef375dd35 100644
--- a/app/views/admin/topics/_form.html.haml
+++ b/app/views/admin/topics/_form.html.haml
@@ -27,7 +27,7 @@
= topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s90')
= render 'shared/choose_avatar_button', f: f
- if @topic.avatar?
- .js-remove-topic-avatar{ data: { path: admin_topic_avatar_path(@topic) } }
+ .js-remove-topic-avatar{ data: { path: admin_topic_avatar_path(@topic), name: @topic.name } }
- if @topic.new_record?
.form-actions
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 34e46807fb6..7bfdaac72c4 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,3 +1,3 @@
= form_for [@project, @issue],
- html: { class: 'issue-form common-note-form gl-mt-3 js-quick-submit gl-show-field-errors' } do |f|
+ html: { class: 'issue-form common-note-form gl-mt-3 js-quick-submit gl-show-field-errors', data: issues_form_data(@project) } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
diff --git a/config/feature_flags/development/block_namespace_serialization.yml b/config/feature_flags/development/block_namespace_serialization.yml
new file mode 100644
index 00000000000..5152789c3fe
--- /dev/null
+++ b/config/feature_flags/development/block_namespace_serialization.yml
@@ -0,0 +1,8 @@
+---
+name: block_namespace_serialization
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82661
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355553
+milestone: '14.9'
+type: development
+group: group::global search
+default_enabled: false
diff --git a/db/fixtures/development/32_crm.rb b/db/fixtures/development/32_crm.rb
index bad2fc56ed3..13e4e82cd2d 100644
--- a/db/fixtures/development/32_crm.rb
+++ b/db/fixtures/development/32_crm.rb
@@ -29,7 +29,7 @@ class Gitlab::Seeder::Crm
group_id: group.id,
first_name: first_name,
last_name: last_name,
- email: "#{first_name}.#{last_name}@example.org",
+ email: "#{first_name}.#{last_name}-#{index}@example.org",
organization_id: organization_id
)
diff --git a/db/migrate/20220314184009_create_protected_environment_approval_rules.rb b/db/migrate/20220314184009_create_protected_environment_approval_rules.rb
new file mode 100644
index 00000000000..f0db2c004ed
--- /dev/null
+++ b/db/migrate/20220314184009_create_protected_environment_approval_rules.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class CreateProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
+ def up
+ create_table :protected_environment_approval_rules do |t|
+ t.references :protected_environment,
+ index: { name: :index_approval_rule_on_protected_environment_id },
+ foreign_key: { to_table: :protected_environments, on_delete: :cascade },
+ null: false
+
+ t.bigint :user_id
+ t.bigint :group_id
+ t.timestamps_with_timezone null: false
+ t.integer :access_level, limit: 2
+ t.integer :required_approvals, null: false, limit: 2
+
+ t.index :user_id
+ t.index :group_id
+
+ t.check_constraint "((access_level IS NOT NULL) AND (group_id IS NULL) AND (user_id IS NULL)) OR " \
+ "((user_id IS NOT NULL) AND (access_level IS NULL) AND (group_id IS NULL)) OR " \
+ "((group_id IS NOT NULL) AND (user_id IS NULL) AND (access_level IS NULL))"
+ t.check_constraint "required_approvals > 0"
+ end
+ end
+
+ def down
+ drop_table :protected_environment_approval_rules
+ end
+end
diff --git a/db/migrate/20220314184109_add_user_fk_to_protected_environment_approval_rules.rb b/db/migrate/20220314184109_add_user_fk_to_protected_environment_approval_rules.rb
new file mode 100644
index 00000000000..c339f465da5
--- /dev/null
+++ b/db/migrate/20220314184109_add_user_fk_to_protected_environment_approval_rules.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddUserFkToProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :protected_environment_approval_rules, :users, column: :user_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :protected_environment_approval_rules, column: :user_id
+ end
+ end
+end
diff --git a/db/migrate/20220314184209_add_group_fk_to_protected_environment_approval_rules.rb b/db/migrate/20220314184209_add_group_fk_to_protected_environment_approval_rules.rb
new file mode 100644
index 00000000000..57c4f7dea33
--- /dev/null
+++ b/db/migrate/20220314184209_add_group_fk_to_protected_environment_approval_rules.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddGroupFkToProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :protected_environment_approval_rules, :namespaces, column: :group_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :protected_environment_approval_rules, column: :group_id
+ end
+ end
+end
diff --git a/db/migrate/20220314194009_add_approval_rule_id_to_deployment_approvals.rb b/db/migrate/20220314194009_add_approval_rule_id_to_deployment_approvals.rb
new file mode 100644
index 00000000000..6e12f568f6e
--- /dev/null
+++ b/db/migrate/20220314194009_add_approval_rule_id_to_deployment_approvals.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddApprovalRuleIdToDeploymentApprovals < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :deployment_approvals, :approval_rule_id, :bigint
+ end
+end
diff --git a/db/migrate/20220314204009_add_approval_rule_fk_to_deployment_approvals.rb b/db/migrate/20220314204009_add_approval_rule_fk_to_deployment_approvals.rb
new file mode 100644
index 00000000000..ba49103b4d3
--- /dev/null
+++ b/db/migrate/20220314204009_add_approval_rule_fk_to_deployment_approvals.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddApprovalRuleFkToDeploymentApprovals < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_deployment_approvals_on_approval_rule_id'
+
+ def up
+ add_concurrent_index :deployment_approvals, :approval_rule_id, name: INDEX_NAME
+ add_concurrent_foreign_key :deployment_approvals, :protected_environment_approval_rules, column: :approval_rule_id, on_delete: :nullify
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :deployment_approvals, column: :approval_rule_id
+ end
+
+ remove_concurrent_index_by_name :deployment_approvals, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20220316112118_update_organizations_name_index_add_id.rb b/db/post_migrate/20220316112118_update_organizations_name_index_add_id.rb
new file mode 100644
index 00000000000..4d78e78e9db
--- /dev/null
+++ b/db/post_migrate/20220316112118_update_organizations_name_index_add_id.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class UpdateOrganizationsNameIndexAddId < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ OLD_INDEX = 'index_customer_relations_organizations_on_unique_name_per_group'
+ NEW_INDEX = 'index_organizations_on_unique_name_per_group'
+
+ def up
+ add_concurrent_index :customer_relations_organizations, 'group_id, lower(name), id', name: NEW_INDEX, unique: true
+
+ remove_concurrent_index_by_name :customer_relations_organizations, OLD_INDEX
+ end
+
+ def down
+ add_concurrent_index :customer_relations_organizations, 'group_id, lower(name)', name: OLD_INDEX, unique: true
+
+ remove_concurrent_index_by_name :customer_relations_organizations, NEW_INDEX
+ end
+end
diff --git a/db/post_migrate/20220316112206_add_contacts_index_on_group_email_and_id.rb b/db/post_migrate/20220316112206_add_contacts_index_on_group_email_and_id.rb
new file mode 100644
index 00000000000..21434a80314
--- /dev/null
+++ b/db/post_migrate/20220316112206_add_contacts_index_on_group_email_and_id.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddContactsIndexOnGroupEmailAndId < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_customer_relations_contacts_on_unique_email_per_group'
+
+ def up
+ add_concurrent_index :customer_relations_contacts, 'group_id, lower(email), id', name: INDEX_NAME, unique: true
+ end
+
+ def down
+ remove_concurrent_index_by_name :customer_relations_contacts, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20220314184009 b/db/schema_migrations/20220314184009
new file mode 100644
index 00000000000..1e00cd48937
--- /dev/null
+++ b/db/schema_migrations/20220314184009
@@ -0,0 +1 @@
+258c7a3409aea1c713c2ddd6679de586e7548ce4d7c0811db1d4903f2794c660 \ No newline at end of file
diff --git a/db/schema_migrations/20220314184109 b/db/schema_migrations/20220314184109
new file mode 100644
index 00000000000..60fefe61de5
--- /dev/null
+++ b/db/schema_migrations/20220314184109
@@ -0,0 +1 @@
+85be80bb8c929d017fedfe66c1f18e4a0dbd7dd8f3b683ded60213e621ec06f4 \ No newline at end of file
diff --git a/db/schema_migrations/20220314184209 b/db/schema_migrations/20220314184209
new file mode 100644
index 00000000000..dfe5e7b5888
--- /dev/null
+++ b/db/schema_migrations/20220314184209
@@ -0,0 +1 @@
+41e7a36164fe3b1b582149d9cfbefc6ee2ce804ac85207f918c064b0ef738b53 \ No newline at end of file
diff --git a/db/schema_migrations/20220314194009 b/db/schema_migrations/20220314194009
new file mode 100644
index 00000000000..e836855b8eb
--- /dev/null
+++ b/db/schema_migrations/20220314194009
@@ -0,0 +1 @@
+e58b89906cd09577c1a773768e4cf3f97b870744e4ee6a323e0276895dff0de5 \ No newline at end of file
diff --git a/db/schema_migrations/20220314204009 b/db/schema_migrations/20220314204009
new file mode 100644
index 00000000000..ac1effdd471
--- /dev/null
+++ b/db/schema_migrations/20220314204009
@@ -0,0 +1 @@
+e2fa0265f3c14c8e6f08a4ffc4b682d8805fa634bac66c578684faaee97541cf \ No newline at end of file
diff --git a/db/schema_migrations/20220316112118 b/db/schema_migrations/20220316112118
new file mode 100644
index 00000000000..da38f5f9fef
--- /dev/null
+++ b/db/schema_migrations/20220316112118
@@ -0,0 +1 @@
+659accb8efe0223028a74346ecf3aa5b649cda825ac7941bc932bc1d7e6f8d9a \ No newline at end of file
diff --git a/db/schema_migrations/20220316112206 b/db/schema_migrations/20220316112206
new file mode 100644
index 00000000000..5605ebf34b9
--- /dev/null
+++ b/db/schema_migrations/20220316112206
@@ -0,0 +1 @@
+d24c5a5414e44385a132e8f342cb67cc5a7c5fe4bfcc4c15c473397076350bc2 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e00d8d86b9f..124811f74b3 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14270,6 +14270,7 @@ CREATE TABLE deployment_approvals (
updated_at timestamp with time zone NOT NULL,
status smallint NOT NULL,
comment text,
+ approval_rule_id bigint,
CONSTRAINT check_e2eb6a17d8 CHECK ((char_length(comment) <= 255))
);
@@ -19681,6 +19682,28 @@ CREATE SEQUENCE protected_branches_id_seq
ALTER SEQUENCE protected_branches_id_seq OWNED BY protected_branches.id;
+CREATE TABLE protected_environment_approval_rules (
+ id bigint NOT NULL,
+ protected_environment_id bigint NOT NULL,
+ user_id bigint,
+ group_id bigint,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ access_level smallint,
+ required_approvals smallint NOT NULL,
+ CONSTRAINT chk_rails_bed75249bc CHECK ((((access_level IS NOT NULL) AND (group_id IS NULL) AND (user_id IS NULL)) OR ((user_id IS NOT NULL) AND (access_level IS NULL) AND (group_id IS NULL)) OR ((group_id IS NOT NULL) AND (user_id IS NULL) AND (access_level IS NULL)))),
+ CONSTRAINT chk_rails_cfa90ae3b5 CHECK ((required_approvals > 0))
+);
+
+CREATE SEQUENCE protected_environment_approval_rules_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE protected_environment_approval_rules_id_seq OWNED BY protected_environment_approval_rules.id;
+
CREATE TABLE protected_environment_deploy_access_levels (
id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -22950,6 +22973,8 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels ALTER COLUMN id SET DE
ALTER TABLE ONLY protected_branches ALTER COLUMN id SET DEFAULT nextval('protected_branches_id_seq'::regclass);
+ALTER TABLE ONLY protected_environment_approval_rules ALTER COLUMN id SET DEFAULT nextval('protected_environment_approval_rules_id_seq'::regclass);
+
ALTER TABLE ONLY protected_environment_deploy_access_levels ALTER COLUMN id SET DEFAULT nextval('protected_environment_deploy_access_levels_id_seq'::regclass);
ALTER TABLE ONLY protected_environments ALTER COLUMN id SET DEFAULT nextval('protected_environments_id_seq'::regclass);
@@ -25066,6 +25091,9 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels
ALTER TABLE ONLY protected_branches
ADD CONSTRAINT protected_branches_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY protected_environment_approval_rules
+ ADD CONSTRAINT protected_environment_approval_rules_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY protected_environment_deploy_access_levels
ADD CONSTRAINT protected_environment_deploy_access_levels_pkey PRIMARY KEY (id);
@@ -26707,6 +26735,8 @@ CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON approv
CREATE UNIQUE INDEX index_approval_rule_name_for_sectional_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id, name, section) WHERE (rule_type = 2);
+CREATE INDEX index_approval_rule_on_protected_environment_id ON protected_environment_approval_rules USING btree (protected_environment_id);
+
CREATE INDEX index_approval_rules_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id) WHERE (rule_type = 2);
CREATE INDEX index_approvals_on_merge_request_id ON approvals USING btree (merge_request_id);
@@ -27273,7 +27303,7 @@ CREATE INDEX index_customer_relations_contacts_on_group_id ON customer_relations
CREATE INDEX index_customer_relations_contacts_on_organization_id ON customer_relations_contacts USING btree (organization_id);
-CREATE UNIQUE INDEX index_customer_relations_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name));
+CREATE UNIQUE INDEX index_customer_relations_contacts_on_unique_email_per_group ON customer_relations_contacts USING btree (group_id, lower(email), id);
CREATE UNIQUE INDEX index_cycle_analytics_stage_event_hashes_on_hash_sha_256 ON analytics_cycle_analytics_stage_event_hashes USING btree (hash_sha256);
@@ -27343,6 +27373,8 @@ CREATE INDEX index_deploy_tokens_on_token_and_expires_at_and_id ON deploy_tokens
CREATE UNIQUE INDEX index_deploy_tokens_on_token_encrypted ON deploy_tokens USING btree (token_encrypted);
+CREATE INDEX index_deployment_approvals_on_approval_rule_id ON deployment_approvals USING btree (approval_rule_id);
+
CREATE UNIQUE INDEX index_deployment_approvals_on_deployment_id_and_user_id ON deployment_approvals USING btree (deployment_id, user_id);
CREATE INDEX index_deployment_approvals_on_user_id ON deployment_approvals USING btree (user_id);
@@ -28393,6 +28425,8 @@ CREATE UNIQUE INDEX index_ops_feature_flags_issues_on_feature_flag_id_and_issue_
CREATE UNIQUE INDEX index_ops_strategies_user_lists_on_strategy_id_and_user_list_id ON operations_strategies_user_lists USING btree (strategy_id, user_list_id);
+CREATE UNIQUE INDEX index_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name), id);
+
CREATE INDEX index_packages_build_infos_on_pipeline_id ON packages_build_infos USING btree (pipeline_id);
CREATE INDEX index_packages_build_infos_package_id_pipeline_id ON packages_build_infos USING btree (package_id, pipeline_id);
@@ -28785,6 +28819,10 @@ CREATE INDEX index_protected_branch_unprotect_access_levels_on_user_id ON protec
CREATE INDEX index_protected_branches_on_project_id ON protected_branches USING btree (project_id);
+CREATE INDEX index_protected_environment_approval_rules_on_group_id ON protected_environment_approval_rules USING btree (group_id);
+
+CREATE INDEX index_protected_environment_approval_rules_on_user_id ON protected_environment_approval_rules USING btree (user_id);
+
CREATE INDEX index_protected_environment_deploy_access ON protected_environment_deploy_access_levels USING btree (protected_environment_id);
CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON protected_environment_deploy_access_levels USING btree (group_id);
@@ -31170,6 +31208,9 @@ ALTER TABLE ONLY ci_pipelines
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_3d674b9f23 FOREIGN KEY (updated_state_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY protected_environment_approval_rules
+ ADD CONSTRAINT fk_405568b491 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY ci_pipeline_schedule_variables
ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE;
@@ -31233,6 +31274,9 @@ ALTER TABLE ONLY project_access_tokens
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_6149611a04 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY deployment_approvals
+ ADD CONSTRAINT fk_61cdbdc5b9 FOREIGN KEY (approval_rule_id) REFERENCES protected_environment_approval_rules(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY dast_profile_schedules
ADD CONSTRAINT fk_61d52aa0e7 FOREIGN KEY (dast_profile_id) REFERENCES dast_profiles(id) ON DELETE CASCADE;
@@ -31272,6 +31316,9 @@ ALTER TABLE ONLY projects
ALTER TABLE ONLY terraform_state_versions
ADD CONSTRAINT fk_6e81384d7f FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY protected_environment_approval_rules
+ ADD CONSTRAINT fk_6ee8249821 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY protected_branch_push_access_levels
ADD CONSTRAINT fk_7111b68cdb FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -32283,6 +32330,9 @@ ALTER TABLE ONLY scim_identities
ALTER TABLE ONLY snippet_user_mentions
ADD CONSTRAINT fk_rails_4d3f96b2cb FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
+ALTER TABLE ONLY protected_environment_approval_rules
+ ADD CONSTRAINT fk_rails_4e554f96f5 FOREIGN KEY (protected_environment_id) REFERENCES protected_environments(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY deployment_clusters
ADD CONSTRAINT fk_rails_4e6243e120 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE;
diff --git a/doc/administration/integration/kroki.md b/doc/administration/integration/kroki.md
index 008d33c6c94..0f02e3783a5 100644
--- a/doc/administration/integration/kroki.md
+++ b/doc/administration/integration/kroki.md
@@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Support for reStructuredText and Textile documents [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324766) in GitLab 13.12.
When [Kroki](https://kroki.io) integration is enabled and configured in
-GitLab you can use it to create diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents.
+GitLab, you can use it to create diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents.
## Kroki Server
diff --git a/doc/api/linked_epics.md b/doc/api/linked_epics.md
index 89168c344f3..2f46786251c 100644
--- a/doc/api/linked_epics.md
+++ b/doc/api/linked_epics.md
@@ -88,3 +88,120 @@ Example response:
}
]
```
+
+## Create a related epic link
+
+Create a two-way relation between two epics. The user must be allowed to
+update both epics to succeed.
+
+```plaintext
+POST /groups/:id/epics/:epic_iid/related_epics
+```
+
+Supported attributes:
+
+| Attribute | Type | Required | Description |
+|---------------------|----------------|-----------------------------|---------------------------------------|
+| `epic_iid` | integer | **{check-circle}** Yes | Internal ID of a group's epic. |
+| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user. |
+| `target_epic_iid` | integer/string | **{check-circle}** Yes | Internal ID of a target group's epic. |
+| `target_group_id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the target group](index.md#namespaced-path-encoding). |
+| `link_type` | string | **{dotted-circle}** No | Type of the relation (`relates_to`, `blocks`, `is_blocked_by`), defaults to `relates_to`. |
+
+Example request:
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/26/epics/1/related_epics?target_group_id=26&target_epic_iid=5"
+```
+
+Example response:
+
+```json
+{
+ "source_epic": {
+ "id": 21,
+ "iid": 1,
+ "color": "#1068bf",
+ "text_color": "#FFFFFF",
+ "group_id": 26,
+ "parent_id": null,
+ "parent_iid": null,
+ "title": "Aspernatur recusandae distinctio omnis et qui est iste.",
+ "description": "some description",
+ "confidential": false,
+ "author": {
+ "id": 15,
+ "username": "trina",
+ "name": "Theresia Robel",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/085e28df717e16484cbf6ceca75e9a93?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/trina"
+ },
+ "start_date": null,
+ "end_date": null,
+ "due_date": null,
+ "state": "opened",
+ "web_url": "http://gitlab.example.com/groups/flightjs/-/epics/1",
+ "references": {
+ "short": "&1",
+ "relative": "&1",
+ "full": "flightjs&1"
+ },
+ "created_at": "2022-01-31T15:10:44.988Z",
+ "updated_at": "2022-03-16T09:32:35.712Z",
+ "closed_at": null,
+ "labels": [],
+ "upvotes": 0,
+ "downvotes": 0,
+ "_links": {
+ "self": "http://gitlab.example.com/api/v4/groups/26/epics/1",
+ "epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/1/issues",
+ "group": "http://gitlab.example.com/api/v4/groups/26",
+ "parent": null
+ }
+ },
+ "target_epic": {
+ "id": 25,
+ "iid": 5,
+ "color": "#1068bf",
+ "text_color": "#FFFFFF",
+ "group_id": 26,
+ "parent_id": null,
+ "parent_iid": null,
+ "title": "Aut assumenda id nihil distinctio fugiat vel numquam est.",
+ "description": "some description",
+ "confidential": false,
+ "author": {
+ "id": 3,
+ "username": "valerie",
+ "name": "Erika Wolf",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/9ef7666abb101418a4716a8ed4dded80?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/valerie"
+ },
+ "start_date": null,
+ "end_date": null,
+ "due_date": null,
+ "state": "opened",
+ "web_url": "http://gitlab.example.com/groups/flightjs/-/epics/5",
+ "references": {
+ "short": "&5",
+ "relative": "&5",
+ "full": "flightjs&5"
+ },
+ "created_at": "2022-01-31T15:10:45.080Z",
+ "updated_at": "2022-03-16T09:32:35.842Z",
+ "closed_at": null,
+ "labels": [],
+ "upvotes": 0,
+ "downvotes": 0,
+ "_links": {
+ "self": "http://gitlab.example.com/api/v4/groups/26/epics/5",
+ "epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/5/issues",
+ "group": "http://gitlab.example.com/api/v4/groups/26",
+ "parent": null
+ }
+ },
+ "link_type": "relates_to"
+}
+```
diff --git a/doc/api/users.md b/doc/api/users.md
index de9af59de93..0bf528c2929 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -99,6 +99,8 @@ GET /users?exclude_external=true
### For admins
+> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
+
```plaintext
GET /users
```
@@ -151,7 +153,8 @@ GET /users
"external": false,
"private_profile": false,
"current_sign_in_ip": "196.165.1.102",
- "last_sign_in_ip": "172.127.2.22"
+ "last_sign_in_ip": "172.127.2.22",
+ "namespace_id": 1
},
{
"id": 2,
@@ -185,7 +188,8 @@ GET /users
"external": false,
"private_profile": false,
"current_sign_in_ip": "10.165.1.102",
- "last_sign_in_ip": "172.127.2.22"
+ "last_sign_in_ip": "172.127.2.22",
+ "namespace_id": 2
}
]
```
@@ -300,6 +304,8 @@ Parameters:
### For admin
+> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
+
```plaintext
GET /users/:id
```
@@ -355,7 +361,8 @@ Example Responses:
"last_sign_in_ip": "172.127.2.22",
"plan": "gold",
"trial": true,
- "sign_in_count": 1337
+ "sign_in_count": 1337,
+ "namespace_id": 1
}
```
@@ -404,6 +411,8 @@ GET /users/:id?with_custom_attributes=true
## User creation
+> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
+
Creates a new user. Note only administrators can create new
users. Either `password`, `reset_password`, or `force_random_password`
must be specified. If `reset_password` and `force_random_password` are
@@ -459,6 +468,8 @@ Parameters:
## User modification
+> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
+
Modifies an existing user. Only administrators can change attributes of a user.
```plaintext
@@ -583,6 +594,8 @@ GET /user
## List current user (for admins)
+> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
+
```plaintext
GET /user
```
@@ -632,7 +645,8 @@ Parameters:
"private_profile": false,
"commit_email": "john-codes@example.com",
"current_sign_in_ip": "196.165.1.102",
- "last_sign_in_ip": "172.127.2.22"
+ "last_sign_in_ip": "172.127.2.22",
+ "namespace_id": 1
}
```
diff --git a/doc/development/service_ping/index.md b/doc/development/service_ping/index.md
index 1db3c409151..1968b12f6ee 100644
--- a/doc/development/service_ping/index.md
+++ b/doc/development/service_ping/index.md
@@ -598,6 +598,9 @@ To upload payload manually:
1. Select **Choose file** and choose the file from p5.
1. Select **Upload**.
+The uploaded file is encrypted and sent using secure [HTTPS protocol](https://en.wikipedia.org/wiki/HTTPS). HTTPS creates a secure
+communication channel between web browser and the server, and protects transmitted data against man-in-the-middle attacks.
+
## Monitoring
Service Ping reporting process state is monitored with [internal SiSense dashboard](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health).
diff --git a/lib/api/entities/user_with_admin.rb b/lib/api/entities/user_with_admin.rb
index e148a5c45b5..f9c1a646a4f 100644
--- a/lib/api/entities/user_with_admin.rb
+++ b/lib/api/entities/user_with_admin.rb
@@ -5,6 +5,7 @@ module API
class UserWithAdmin < UserPublic
expose :admin?, as: :is_admin
expose :note
+ expose :namespace_id
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 0f710e0a307..ad6a31a4715 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -120,8 +120,11 @@ module API
users = reorder_users(users)
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
- users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
- users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin
+
+ if entity == Entities::UserWithAdmin
+ users = users.preload(:identities, :u2f_registrations, :webauthn_registrations, :namespace)
+ end
+
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
users = users.preload(:user_detail)
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index dcd78bfd84f..850eadf9b89 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -435,6 +435,7 @@ protected_branches: :gitlab_main
protected_branch_merge_access_levels: :gitlab_main
protected_branch_push_access_levels: :gitlab_main
protected_branch_unprotect_access_levels: :gitlab_main
+protected_environment_approval_rules: :gitlab_main
protected_environment_deploy_access_levels: :gitlab_main
protected_environments: :gitlab_main
protected_tag_create_access_levels: :gitlab_main
diff --git a/lib/gitlab/import_export/fast_hash_serializer.rb b/lib/gitlab/import_export/fast_hash_serializer.rb
index e5d52f945b5..d049609187b 100644
--- a/lib/gitlab/import_export/fast_hash_serializer.rb
+++ b/lib/gitlab/import_export/fast_hash_serializer.rb
@@ -92,7 +92,7 @@ module Gitlab
def simple_serialize
subject.as_json(
- tree.merge(include: nil, preloads: nil))
+ tree.merge(include: nil, preloads: nil, unsafe: true))
end
def serialize_includes
diff --git a/lib/gitlab/import_export/json/streaming_serializer.rb b/lib/gitlab/import_export/json/streaming_serializer.rb
index 55b8c1d4531..d646a1a8207 100644
--- a/lib/gitlab/import_export/json/streaming_serializer.rb
+++ b/lib/gitlab/import_export/json/streaming_serializer.rb
@@ -37,7 +37,7 @@ module Gitlab
def serialize_root(exportable_path = @exportable_path)
attributes = exportable.as_json(
- relations_schema.merge(include: nil, preloads: nil))
+ relations_schema.merge(include: nil, preloads: nil, unsafe: true))
json_writer.write_attributes(exportable_path, attributes)
end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 9374c5c8f8f..5d5d10b42f0 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -27,6 +27,11 @@ module Mattermost
LEASE_TIMEOUT = 60
+ Request = Struct.new(:parameters, keyword_init: true) do
+ def method_missing(method_name, *args, &block)
+ end
+ end
+
attr_accessor :current_resource_owner, :token, :base_uri
def initialize(current_user)
@@ -64,7 +69,7 @@ module Mattermost
end
def request
- @request ||= OpenStruct.new(parameters: params)
+ @request ||= Request.new(parameters: params)
end
def params
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3efa9efe8d7..2d0cc47c5a6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9424,9 +9424,6 @@ msgstr ""
msgid "Confirm new password"
msgstr ""
-msgid "Confirm remove avatar"
-msgstr ""
-
msgid "Confirm user"
msgstr ""
@@ -22703,6 +22700,9 @@ msgstr ""
msgid "Manual"
msgstr ""
+msgid "Manual iteration cadences are deprecated. Only automatic iteration cadences are allowed."
+msgstr ""
+
msgid "ManualOrdering|Couldn't save the order of the issues"
msgstr ""
@@ -30947,6 +30947,9 @@ msgstr ""
msgid "Remove time estimate"
msgstr ""
+msgid "Remove topic avatar"
+msgstr ""
+
msgid "Remove user"
msgstr ""
@@ -39156,6 +39159,9 @@ msgstr ""
msgid "Topic avatar"
msgstr ""
+msgid "Topic avatar for %{name} will be removed. This cannot be undone."
+msgstr ""
+
msgid "Topic name"
msgstr ""
@@ -39249,6 +39255,9 @@ msgstr ""
msgid "TransferGroup|Database is not supported."
msgstr ""
+msgid "TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group."
+msgstr ""
+
msgid "TransferGroup|Group contains projects with NPM packages."
msgstr ""
diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb
index 5e7ff34463c..fa4fdf805ec 100644
--- a/spec/deprecation_toolkit_env.rb
+++ b/spec/deprecation_toolkit_env.rb
@@ -56,11 +56,8 @@ module DeprecationToolkitEnv
# In this case, we recommend to add a silence together with an issue to patch or update
# the dependency causing the problem.
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
- #
- # - ruby/lib/grpc/generic/interceptors.rb: https://gitlab.com/gitlab-org/gitlab/-/issues/339305
def self.allowed_kwarg_warning_paths
%w[
- ruby/lib/grpc/generic/interceptors.rb
]
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/admin.json b/spec/fixtures/api/schemas/public_api/v4/user/admin.json
index f733914fbf8..8d06e16848f 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/admin.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/admin.json
@@ -26,7 +26,8 @@
"can_create_group",
"can_create_project",
"two_factor_enabled",
- "external"
+ "external",
+ "namespace_id"
],
"properties": {
"$ref": "full.json"
diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js
index d4656f0a199..97d257c682c 100644
--- a/spec/frontend/admin/topics/components/remove_avatar_spec.js
+++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js
@@ -1,10 +1,11 @@
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveAvatar from '~/admin/topics/components/remove_avatar.vue';
const modalID = 'fake-id';
const path = 'topic/path/1';
+const name = 'Topic 1';
jest.mock('lodash/uniqueId', () => () => 'fake-id');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -16,10 +17,14 @@ describe('RemoveAvatar', () => {
wrapper = shallowMount(RemoveAvatar, {
provide: {
path,
+ name,
},
directives: {
GlModal: createMockDirective(),
},
+ stubs: {
+ GlSprintf,
+ },
});
};
@@ -55,8 +60,8 @@ describe('RemoveAvatar', () => {
const modal = findModal();
expect(modal.exists()).toBe(true);
- expect(modal.props('title')).toBe('Confirm remove avatar');
- expect(modal.text()).toBe('Avatar will be removed. Are you sure?');
+ expect(modal.props('title')).toBe('Remove topic avatar');
+ expect(modal.text()).toBe(`Topic avatar for ${name} will be removed. This cannot be undone.`);
});
it('contains the correct modal ID', () => {
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 321c61ead1e..99ed18cf5bd 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -1,20 +1,46 @@
import $ from 'jquery';
import IssuableForm from '~/issuable/issuable_form';
-
-function createIssuable() {
- const instance = new IssuableForm($(document.createElement('form')));
-
- instance.titleField = $(document.createElement('input'));
-
- return instance;
-}
+import setWindowLocation from 'helpers/set_window_location_helper';
describe('IssuableForm', () => {
let instance;
+ const createIssuable = (form) => {
+ instance = new IssuableForm(form);
+ };
+
beforeEach(() => {
- instance = createIssuable();
+ setFixtures(`
+ <form>
+ <input name="[title]" />
+ </form>
+ `);
+ createIssuable($('form'));
+ });
+
+ describe('initAutosave', () => {
+ it('creates autosave with the searchTerm included', () => {
+ setWindowLocation('https://gitlab.test/foo?bar=true');
+ const autosave = instance.initAutosave();
+
+ expect(autosave.key.includes('bar=true')).toBe(true);
+ });
+
+ it("creates autosave fields without the searchTerm if it's an issue new form", () => {
+ setFixtures(`
+ <form data-new-issue-path="/issues/new">
+ <input name="[title]" />
+ </form>
+ `);
+ createIssuable($('form'));
+
+ setWindowLocation('https://gitlab.test/issues/new?bar=true');
+
+ const autosave = instance.initAutosave();
+
+ expect(autosave.key.includes('bar=true')).toBe(false);
+ });
});
describe('removeWip', () => {
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index a85b1bd0a48..29e36732450 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -368,6 +368,16 @@ RSpec.describe IssuesHelper do
end
end
+ describe '#issues_form_data' do
+ it 'returns expected result' do
+ expected = {
+ new_issue_path: new_project_issue_path(project)
+ }
+
+ expect(helper.issues_form_data(project)).to include(expected)
+ end
+ end
+
describe '#issue_manual_ordering_class' do
context 'when sorting by relative position' do
before do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 29a19e4cafd..730f9035293 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -665,6 +665,7 @@ protected_environments:
- project
- group
- deploy_access_levels
+- approval_rules
deploy_access_levels:
- protected_environment
- user
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index e2e1b4c28c7..2158076e4b5 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -35,6 +35,12 @@ RSpec.describe Mattermost::Session, type: :request do
it 'makes a request to the oauth uri' do
expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
+
+ it 'returns nill on calling a non exisitng method on request' do
+ return_value = subject.request.method_missing("non_existing_method", "something") do
+ end
+ expect(return_value).to be(nil)
+ end
end
context 'with oauth_uri' do
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index 18896962261..86f868b269e 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe CustomerRelations::Contact, type: :model do
it { is_expected.to validate_length_of(:email).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
- it { is_expected.to validate_uniqueness_of(:email).scoped_to(:group_id) }
+ it { is_expected.to validate_uniqueness_of(:email).case_insensitive.scoped_to(:group_id) }
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
end
@@ -87,6 +87,15 @@ RSpec.describe CustomerRelations::Contact, type: :model do
too_many_emails = described_class::MAX_PLUCK + 1
expect { described_class.find_ids_by_emails(group, Array(0..too_many_emails)) }.to raise_error(ArgumentError)
end
+
+ it 'finds contacts regardless of email casing' do
+ new_contact = create(:contact, group: group, email: "UPPERCASE@example.com")
+ emails = [group_contacts[0].email.downcase, group_contacts[1].email.upcase, new_contact.email]
+
+ contact_ids = described_class.find_ids_by_emails(group, emails)
+
+ expect(contact_ids).to contain_exactly(group_contacts[0].id, group_contacts[1].id, new_contact.id)
+ end
end
describe '#self.exists_for_group?' do
@@ -104,4 +113,33 @@ RSpec.describe CustomerRelations::Contact, type: :model do
end
end
end
+
+ describe '#self.move_to_root_group' do
+ let!(:old_root_group) { create(:group) }
+ let!(:contacts) { create_list(:contact, 4, group: old_root_group) }
+ let!(:project) { create(:project, group: old_root_group) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:issue_contact1) { create(:issue_customer_relations_contact, issue: issue, contact: contacts[0]) }
+ let!(:issue_contact2) { create(:issue_customer_relations_contact, issue: issue, contact: contacts[1]) }
+ let!(:new_root_group) { create(:group) }
+ let!(:dupe_contact1) { create(:contact, group: new_root_group, email: contacts[1].email) }
+ let!(:dupe_contact2) { create(:contact, group: new_root_group, email: contacts[3].email.upcase) }
+
+ before do
+ old_root_group.update!(parent: new_root_group)
+ CustomerRelations::Contact.move_to_root_group(old_root_group)
+ end
+
+ it 'moves contacts with unique emails and deletes the rest' do
+ expect(contacts[0].reload.group_id).to eq(new_root_group.id)
+ expect(contacts[2].reload.group_id).to eq(new_root_group.id)
+ expect { contacts[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { contacts[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'updates issue_contact.contact_id for dupes and leaves the rest untouched' do
+ expect(issue_contact1.reload.contact_id).to eq(contacts[0].id)
+ expect(issue_contact2.reload.contact_id).to eq(dupe_contact1.id)
+ end
+ end
end
diff --git a/spec/models/customer_relations/issue_contact_spec.rb b/spec/models/customer_relations/issue_contact_spec.rb
index f1fb574f86f..221378d26b2 100644
--- a/spec/models/customer_relations/issue_contact_spec.rb
+++ b/spec/models/customer_relations/issue_contact_spec.rb
@@ -92,4 +92,16 @@ RSpec.describe CustomerRelations::IssueContact do
expect { described_class.delete_for_project(project.id) }.to change { described_class.count }.by(-3)
end
end
+
+ describe '.delete_for_group' do
+ let(:project_for_root_group) { create(:project, group: group) }
+
+ it 'destroys all issue_contacts for projects in group and subgroups' do
+ create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project))
+ create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project_for_root_group))
+ create(:issue_customer_relations_contact)
+
+ expect { described_class.delete_for_group(group) }.to change { described_class.count }.by(-4)
+ end
+ end
end
diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb
index 9fe754b7605..06ba9c5b7ad 100644
--- a/spec/models/customer_relations/organization_spec.rb
+++ b/spec/models/customer_relations/organization_spec.rb
@@ -50,4 +50,32 @@ RSpec.describe CustomerRelations::Organization, type: :model do
expect(described_class.find_by_name(group.id, 'TEST')).to eq([organiztion1])
end
end
+
+ describe '#self.move_to_root_group' do
+ let!(:old_root_group) { create(:group) }
+ let!(:organizations) { create_list(:organization, 4, group: old_root_group) }
+ let!(:new_root_group) { create(:group) }
+ let!(:contact1) { create(:contact, group: new_root_group, organization: organizations[0]) }
+ let!(:contact2) { create(:contact, group: new_root_group, organization: organizations[1]) }
+
+ let!(:dupe_organization1) { create(:organization, group: new_root_group, name: organizations[1].name) }
+ let!(:dupe_organization2) { create(:organization, group: new_root_group, name: organizations[3].name.upcase) }
+
+ before do
+ old_root_group.update!(parent: new_root_group)
+ CustomerRelations::Organization.move_to_root_group(old_root_group)
+ end
+
+ it 'moves organizations with unique names and deletes the rest' do
+ expect(organizations[0].reload.group_id).to eq(new_root_group.id)
+ expect(organizations[2].reload.group_id).to eq(new_root_group.id)
+ expect { organizations[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { organizations[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'updates contact.organization_id for dupes and leaves the rest untouched' do
+ expect(contact1.reload.organization_id).to eq(organizations[0].id)
+ expect(contact2.reload.organization_id).to eq(dupe_organization1.id)
+ end
+ end
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 48d8ba975b6..fb8e8a9b5ad 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -1021,4 +1021,54 @@ RSpec.describe Integration do
)
end
end
+
+ describe 'boolean_accessor' do
+ let(:klass) do
+ Class.new(Integration) do
+ boolean_accessor :test_value
+ end
+ end
+
+ let(:integration) { klass.new(properties: { test_value: input }) }
+
+ where(:input, :method_result, :predicate_method_result) do
+ true | true | true
+ false | false | false
+ 1 | true | true
+ 0 | false | false
+ '1' | true | true
+ '0' | false | false
+ 'true' | true | true
+ 'false' | false | false
+ 'foobar' | nil | false
+ '' | nil | false
+ nil | nil | false
+ 'on' | true | true
+ 'off' | false | false
+ 'yes' | true | true
+ 'no' | false | false
+ 'n' | false | false
+ 'y' | true | true
+ 't' | true | true
+ 'f' | false | false
+ end
+
+ with_them do
+ it 'has the correct value' do
+ expect(integration).to have_attributes(
+ test_value: be(method_result),
+ test_value?: be(predicate_method_result)
+ )
+ end
+ end
+
+ it 'returns values when initialized without input' do
+ integration = klass.new
+
+ expect(integration).to have_attributes(
+ test_value: be(nil),
+ test_value?: be(false)
+ )
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ebd153f6f10..f2ea17556a0 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -2230,4 +2230,18 @@ RSpec.describe Namespace do
expect(namespace.storage_enforcement_date).to be(nil)
end
end
+
+ describe 'serialization' do
+ let(:object) { build(:namespace) }
+
+ it_behaves_like 'blocks unsafe serialization'
+
+ context 'when feature flag block_namespace_serialization is disabled' do
+ before do
+ stub_feature_flags(block_namespace_serialization: false)
+ end
+
+ it_behaves_like 'allows unsafe serialization'
+ end
+ end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 7d816b100a2..3bc33a820eb 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -83,19 +83,21 @@ RSpec.describe API::Users do
describe 'GET /users/' do
context 'when unauthenticated' do
- it "does not contain the note of users" do
+ it "does not contain certain fields" do
get api("/users"), params: { username: user.username }
expect(json_response.first).not_to have_key('note')
+ expect(json_response.first).not_to have_key('namespace_id')
end
end
context 'when authenticated' do
context 'as a regular user' do
- it 'does not contain the note of users' do
+ it 'does not contain certain fields' do
get api("/users", user), params: { username: user.username }
expect(json_response.first).not_to have_key('note')
+ expect(json_response.first).not_to have_key('namespace_id')
end
end
@@ -154,6 +156,7 @@ RSpec.describe API::Users do
get api("/user", user)
expect(json_response).not_to have_key('note')
+ expect(json_response).not_to have_key('namespace_id')
end
end
end
@@ -384,6 +387,15 @@ RSpec.describe API::Users do
expect(response).to include_pagination_headers
end
+ it "users contain the `namespace_id` field" do
+ get api("/users", admin)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['namespace_id'] }).to include(user.namespace_id, admin.namespace_id)
+ end
+
it "returns an array of external users" do
create(:user, external: true)
@@ -697,6 +709,14 @@ RSpec.describe API::Users do
expect(json_response['highest_role']).to be(0)
end
+ it 'includes the `namespace_id` field' do
+ get api("/users/#{user.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(response).to match_response_schema('public_api/v4/user/admin')
+ expect(json_response['namespace_id']).to eq(user.namespace_id)
+ end
+
if Gitlab.ee?
it 'does not include values for plan or trial' do
get api("/users/#{user.id}", admin)
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 3a696228382..5bc215b4ac4 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
let_it_be(:user) { create(:user) }
- let_it_be(:new_parent_group) { create(:group, :public) }
+ let_it_be(:new_parent_group) { create(:group, :public, :crm_enabled) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:transfer_service) { described_class.new(group, user) }
@@ -876,5 +876,108 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
end
end
+
+ context 'crm' do
+ let(:root_group) { create(:group, :public) }
+ let(:subgroup) { create(:group, :public, parent: root_group) }
+ let(:another_subgroup) { create(:group, :public, parent: root_group) }
+ let(:subsubgroup) { create(:group, :public, parent: subgroup) }
+
+ let(:root_project) { create(:project, group: root_group) }
+ let(:sub_project) { create(:project, group: subgroup) }
+ let(:another_project) { create(:project, group: another_subgroup) }
+ let(:subsub_project) { create(:project, group: subsubgroup) }
+
+ let!(:contacts) { create_list(:contact, 4, group: root_group) }
+ let!(:organizations) { create_list(:organization, 2, group: root_group) }
+
+ before do
+ create(:issue_customer_relations_contact, contact: contacts[0], issue: create(:issue, project: root_project))
+ create(:issue_customer_relations_contact, contact: contacts[1], issue: create(:issue, project: sub_project))
+ create(:issue_customer_relations_contact, contact: contacts[2], issue: create(:issue, project: another_project))
+ create(:issue_customer_relations_contact, contact: contacts[3], issue: create(:issue, project: subsub_project))
+ root_group.add_owner(user)
+ end
+
+ context 'moving up' do
+ let(:group) { subsubgroup }
+
+ it 'retains issue contacts' do
+ expect { transfer_service.execute(root_group) }
+ .not_to change { CustomerRelations::IssueContact.count }
+ end
+ end
+
+ context 'moving down' do
+ let(:group) { subgroup }
+
+ it 'retains issue contacts' do
+ expect { transfer_service.execute(another_subgroup) }
+ .not_to change { CustomerRelations::IssueContact.count }
+ end
+ end
+
+ context 'moving sideways' do
+ let(:group) { subsubgroup }
+
+ it 'retains issue contacts' do
+ expect { transfer_service.execute(another_subgroup) }
+ .not_to change { CustomerRelations::IssueContact.count }
+ end
+ end
+
+ context 'moving to new root group' do
+ let(:group) { root_group }
+
+ before do
+ new_parent_group.add_owner(user)
+ end
+
+ it 'moves all crm objects' do
+ expect { transfer_service.execute(new_parent_group) }
+ .to change { root_group.contacts.count }.by(-4)
+ .and change { root_group.organizations.count }.by(-2)
+ end
+
+ it 'retains issue contacts' do
+ expect { transfer_service.execute(new_parent_group) }
+ .not_to change { CustomerRelations::IssueContact.count }
+ end
+ end
+
+ context 'moving to a subgroup within a new root group' do
+ let(:group) { root_group }
+ let(:subgroup_in_new_parent_group) { create(:group, parent: new_parent_group) }
+
+ context 'with permission on the root group' do
+ before do
+ new_parent_group.add_owner(user)
+ end
+
+ it 'moves all crm objects' do
+ expect { transfer_service.execute(subgroup_in_new_parent_group) }
+ .to change { root_group.contacts.count }.by(-4)
+ .and change { root_group.organizations.count }.by(-2)
+ end
+
+ it 'retains issue contacts' do
+ expect { transfer_service.execute(subgroup_in_new_parent_group) }
+ .not_to change { CustomerRelations::IssueContact.count }
+ end
+ end
+
+ context 'with permission on the subgroup' do
+ before do
+ subgroup_in_new_parent_group.add_owner(user)
+ end
+
+ it 'raises error' do
+ transfer_service.execute(subgroup_in_new_parent_group)
+
+ expect(transfer_service.error).to eq("Transfer failed: Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
index 6146aae6b9b..9610cdd18a3 100644
--- a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
@@ -70,8 +70,10 @@ shared_examples 'issuable link creation' do
expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to')
end
- it 'returns success status' do
- is_expected.to eq(status: :success)
+ it 'returns success status and created links', :aggregate_failures do
+ expect(subject.keys).to match_array([:status, :created_references])
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:created_references].map(&:target_id)).to match_array([issuable2.id, issuable3.id])
end
it 'creates notes' do