diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-22 15:07:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-22 15:07:28 +0300 |
commit | 4220cf46a314ac1c4d88be13608752bc07bb28fb (patch) | |
tree | 169db13fd06c7622bbcf59b707d57d516f44e299 | |
parent | 4c7e34071eceb05a9ce271354c21de7487e4ff84 (diff) |
Add latest changes from gitlab-org/gitlab@master
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 |