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:
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue43
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue18
-rw-r--r--app/assets/javascripts/ide/constants.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/clientside/actions.js4
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue14
-rw-r--r--app/assets/javascripts/members/constants.js1
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue8
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb11
-rw-r--r--app/controllers/projects/service_ping_controller.rb8
-rw-r--r--app/finders/crm/contacts_finder.rb39
-rw-r--r--app/models/preloaders/single_hierarchy_project_group_plans_preloader.rb17
-rw-r--r--app/services/projects/autocomplete_service.rb5
-rw-r--r--config/feature_flags/development/group_project_api_preload_plans.yml8
-rw-r--r--config/metrics/counts_all/20220122022215_web_ide_previews_success.yml21
-rw-r--r--config/routes/project.rb2
-rw-r--r--db/post_migrate/20220121214752_remove_projects_ci_stages_project_id_fk.rb20
-rw-r--r--db/schema_migrations/202201212147521
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/feature_flags.md13
-rw-r--r--doc/ci/pipelines/img/merged_result_pipeline.pngbin7374 -> 0 bytes
-rw-r--r--doc/ci/pipelines/merge_trains.md18
-rw-r--r--doc/ci/pipelines/pipelines_for_merged_results.md133
-rw-r--r--doc/ci/troubleshooting.md2
-rw-r--r--doc/user/packages/container_registry/reduce_container_registry_storage.md2
-rw-r--r--doc/user/packages/dependency_proxy/index.md61
-rw-r--r--doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md74
-rw-r--r--doc/user/packages/package_registry/index.md60
-rw-r--r--doc/user/packages/package_registry/reduce_package_registry_storage.md52
-rw-r--r--doc/user/project/protected_branches.md2
-rw-r--r--lib/api/groups.rb7
-rw-r--r--lib/api/projects_relation_builder.rb1
-rw-r--r--lib/gitlab/database/gitlab_loose_foreign_keys.yml4
-rw-r--r--lib/gitlab/usage_data_counters/web_ide_counter.rb8
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/controllers/projects/autocomplete_sources_controller_spec.rb60
-rw-r--r--spec/controllers/projects/service_ping_controller_spec.rb27
-rw-r--r--spec/features/boards/boards_spec.rb6
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb5
-rw-r--r--spec/finders/crm/contacts_finder_spec.rb70
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js22
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js17
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js5
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js5
-rw-r--r--spec/models/ci/stage_spec.rb7
-rw-r--r--spec/presenters/blob_presenter_spec.rb2
-rw-r--r--spec/requests/api/api_spec.rb2
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb2
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb2
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_expiration_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/grafana_integration_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/designs/designs_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/repository/blobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/repository_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/tree/tree_spec.rb2
-rw-r--r--spec/requests/api/markdown_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb2
-rw-r--r--spec/requests/api/project_export_spec.rb2
-rw-r--r--spec/requests/api/project_snapshots_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb4
-rw-r--r--spec/requests/boards/lists_controller_spec.rb2
-rw-r--r--spec/requests/import/gitlab_projects_controller_spec.rb2
-rw-r--r--spec/requests/projects/clusters/integrations_controller_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests/creations_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests_spec.rb2
-rw-r--r--spec/requests/projects/metrics_dashboard_spec.rb2
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb2
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb26
83 files changed, 708 insertions, 291 deletions
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 6b7c08d05a5..24071c6f0b4 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui';
+import { GlButton, GlDrawer, GlLabel, GlModal, GlModalDirective } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
@@ -11,8 +11,14 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
listSettingsText: __('List settings'),
+ i18n: {
+ modalAction: __('Remove list'),
+ modalCopy: __('Are you sure you want to remove this list?'),
+ modalCancel: __('Cancel'),
+ },
components: {
GlButton,
+ GlModal,
GlDrawer,
GlLabel,
MountingPortal,
@@ -21,6 +27,9 @@ export default {
BoardSettingsListTypes: () =>
import('ee_component/boards/components/board_settings_list_types.vue'),
},
+ directives: {
+ GlModal: GlModalDirective,
+ },
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
inject: ['canAdminList', 'scopedLabelsAvailable'],
inheritAttrs: false,
@@ -29,6 +38,7 @@ export default {
ListType,
};
},
+ modalId: 'board-settings-sidebar-modal',
computed: {
...mapGetters(['isSidebarOpen', 'isEpicBoard']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
@@ -59,16 +69,16 @@ export default {
},
methods: {
...mapActions(['unsetActiveId', 'removeList']),
+ handleModalPrimary() {
+ this.deleteBoard();
+ },
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
},
deleteBoard() {
- // eslint-disable-next-line no-alert
- if (window.confirm(__('Are you sure you want to remove this list?'))) {
- this.track('click_button', { label: 'remove_list' });
- this.removeList(this.activeId);
- this.unsetActiveId();
- }
+ this.track('click_button', { label: 'remove_list' });
+ this.removeList(this.activeId);
+ this.unsetActiveId();
},
},
};
@@ -92,11 +102,10 @@ export default {
<template #header>
<div v-if="canAdminList && activeList.id" class="gl-mt-3">
<gl-button
+ v-gl-modal="$options.modalId"
variant="danger"
category="secondary"
size="small"
- data-testid="remove-list"
- @click.stop="deleteBoard"
>{{ __('Remove list') }}
</gl-button>
</div>
@@ -122,5 +131,21 @@ export default {
/>
</template>
</gl-drawer>
+ <gl-modal
+ :modal-id="$options.modalId"
+ :title="$options.i18n.modalAction"
+ size="sm"
+ :action-primary="{
+ text: $options.i18n.modalAction,
+ attributes: [{ variant: 'danger' }],
+ }"
+ :action-secondary="{
+ text: $options.i18n.modalCancel,
+ attributes: [{ variant: 'default' }],
+ }"
+ @primary="handleModalPrimary"
+ >
+ <p>{{ $options.i18n.modalCopy }}</p>
+ </gl-modal>
</mounting-portal>
</template>
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 13f2e775fc3..b1f6f2c87b9 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -4,7 +4,12 @@ import { listen } from 'codesandbox-api';
import { isEmpty, debounce } from 'lodash';
import { Manager } from 'smooshpack';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { packageJsonPath, LIVE_PREVIEW_DEBOUNCE } from '../../constants';
+import {
+ packageJsonPath,
+ LIVE_PREVIEW_DEBOUNCE,
+ PING_USAGE_PREVIEW_KEY,
+ PING_USAGE_PREVIEW_SUCCESS_KEY,
+} from '../../constants';
import eventHub from '../../eventhub';
import { createPathWithExt } from '../../utils';
import Navigator from './navigator.vue';
@@ -62,6 +67,15 @@ export default {
};
},
},
+ watch: {
+ sandpackReady: {
+ handler(val) {
+ if (val) {
+ this.pingUsage(PING_USAGE_PREVIEW_SUCCESS_KEY);
+ }
+ },
+ },
+ },
mounted() {
this.onFilesChangeCallback = debounce(() => this.update(), LIVE_PREVIEW_DEBOUNCE);
eventHub.$on('ide.files.change', this.onFilesChangeCallback);
@@ -101,7 +115,7 @@ export default {
initPreview() {
if (!this.mainEntry) return null;
- this.pingUsage();
+ this.pingUsage(PING_USAGE_PREVIEW_KEY);
return this.loadFileContent(this.mainEntry)
.then(() => this.$nextTick())
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 775b6906498..bfe4c3ac271 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -114,3 +114,7 @@ export const LIVE_PREVIEW_DEBOUNCE = 2000;
export const MAX_MR_FILES_AUTO_OPEN = 10;
export const DEFAULT_BRANCH = 'main';
+
+// Ping Usage Metrics Keys
+export const PING_USAGE_PREVIEW_KEY = 'web_ide_clientside_preview';
+export const PING_USAGE_PREVIEW_SUCCESS_KEY = 'web_ide_clientside_preview_success';
diff --git a/app/assets/javascripts/ide/stores/modules/clientside/actions.js b/app/assets/javascripts/ide/stores/modules/clientside/actions.js
index e36419cd7eb..1a8e665867f 100644
--- a/app/assets/javascripts/ide/stores/modules/clientside/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/clientside/actions.js
@@ -1,9 +1,9 @@
import axios from '~/lib/utils/axios_utils';
-export const pingUsage = ({ rootGetters }) => {
+export const pingUsage = ({ rootGetters }, metricName) => {
const { web_url: projectUrl } = rootGetters.currentProject;
- const url = `${projectUrl}/service_ping/web_ide_clientside_preview`;
+ const url = `${projectUrl}/service_ping/${metricName}`;
return axios.post(url);
};
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index e09d16cf680..b4ba9aa36e7 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -11,7 +11,9 @@ import {
ACTIVE_TAB_QUERY_PARAM_NAME,
TAB_QUERY_PARAM_VALUES,
MEMBER_STATE_AWAITING,
+ MEMBER_STATE_ACTIVE,
USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_AWAITING_USER_SIGNUP,
BADGE_LABELS_PENDING_OWNER_APPROVAL,
} from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
@@ -154,8 +156,12 @@ export default {
* @see {@link ~/app/serializers/member_entity.rb}
* @returns {boolean}
*/
- isNewUser(memberInviteMetadata) {
- return memberInviteMetadata && !memberInviteMetadata.userState;
+ isNewUser(memberInviteMetadata, memberState) {
+ return (
+ memberInviteMetadata &&
+ !memberInviteMetadata.userState &&
+ memberState !== MEMBER_STATE_ACTIVE
+ );
},
/**
* Returns whether the user is awaiting root approval
@@ -204,6 +210,10 @@ export default {
* @returns {string}
*/
inviteBadge(memberInviteMetadata, memberState) {
+ if (this.isNewUser(memberInviteMetadata, memberState)) {
+ return BADGE_LABELS_AWAITING_USER_SIGNUP;
+ }
+
if (this.shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState)) {
return BADGE_LABELS_PENDING_OWNER_APPROVAL;
}
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 62241eaed04..273f1acebc7 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -111,6 +111,7 @@ export const MEMBER_STATE_CREATED = 0;
export const MEMBER_STATE_AWAITING = 1;
export const MEMBER_STATE_ACTIVE = 2;
+export const BADGE_LABELS_AWAITING_USER_SIGNUP = __('Awaiting user signup');
export const BADGE_LABELS_PENDING_OWNER_APPROVAL = __('Pending owner approval');
export const DAYS_TO_EXPIRE_SOON = 7;
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 8e32c3b3073..ddf72587ba3 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -5,6 +5,7 @@ import DraftNote from '~/batch_comments/components/draft_note.vue';
import createFlash from '~/flash';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { s__, __ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -170,12 +171,13 @@ export default {
this.expandDiscussion({ discussionId: this.discussion.id });
}
},
- cancelReplyForm(shouldConfirm, isDirty) {
+ async cancelReplyForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
- // eslint-disable-next-line no-alert
- if (!window.confirm(msg)) {
+ const confirmed = await confirmAction(msg);
+
+ if (!confirmed) {
return;
}
}
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index cf432cfb429..f678e19d05d 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -2,8 +2,9 @@
class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :authorize_read_milestone!, only: :milestones
+ before_action :authorize_read_crm_contact!, only: :contacts
- feature_category :team_planning, [:issues, :labels, :milestones, :commands]
+ feature_category :team_planning, [:issues, :labels, :milestones, :commands, :contacts]
feature_category :code_review, [:merge_requests]
feature_category :users, [:members]
feature_category :snippets, [:snippets]
@@ -38,6 +39,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
render json: autocomplete_service.snippets
end
+ def contacts
+ render json: autocomplete_service.contacts
+ end
+
private
def autocomplete_service
@@ -49,6 +54,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
.new(project, current_user)
.execute(params[:type], params[:type_id])
end
+
+ def authorize_read_crm_contact!
+ render_404 unless can?(current_user, :read_crm_contact, project.root_ancestor)
+ end
end
Projects::AutocompleteSourcesController.prepend_mod_with('Projects::AutocompleteSourcesController')
diff --git a/app/controllers/projects/service_ping_controller.rb b/app/controllers/projects/service_ping_controller.rb
index 00530c09be8..368da8d1ef2 100644
--- a/app/controllers/projects/service_ping_controller.rb
+++ b/app/controllers/projects/service_ping_controller.rb
@@ -13,6 +13,14 @@ class Projects::ServicePingController < Projects::ApplicationController
head(200)
end
+ def web_ide_clientside_preview_success
+ return render_404 unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
+
+ Gitlab::UsageDataCounters::WebIdeCounter.increment_previews_success_count
+
+ head(200)
+ end
+
def web_ide_pipelines_count
Gitlab::UsageDataCounters::WebIdeCounter.increment_pipelines_count
diff --git a/app/finders/crm/contacts_finder.rb b/app/finders/crm/contacts_finder.rb
new file mode 100644
index 00000000000..c2d44bec27b
--- /dev/null
+++ b/app/finders/crm/contacts_finder.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+# Finder for retrieving contacts scoped to a group
+#
+# Arguments:
+# current_user - user performing the action. Must have the correct permission level for the group.
+# params:
+# group: Group, required
+module Crm
+ class ContactsFinder
+ include Gitlab::Allowable
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :params, :current_user
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return CustomerRelations::Contact.none unless root_group
+
+ root_group.contacts
+ end
+
+ private
+
+ def root_group
+ strong_memoize(:root_group) do
+ group = params[:group]&.root_ancestor
+
+ next unless can?(@current_user, :read_crm_contact, group)
+
+ group
+ end
+ end
+ end
+end
diff --git a/app/models/preloaders/single_hierarchy_project_group_plans_preloader.rb b/app/models/preloaders/single_hierarchy_project_group_plans_preloader.rb
new file mode 100644
index 00000000000..179214666ed
--- /dev/null
+++ b/app/models/preloaders/single_hierarchy_project_group_plans_preloader.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class SingleHierarchyProjectGroupPlansPreloader
+ attr_reader :projects
+
+ def initialize(projects_relation)
+ @projects = projects_relation
+ end
+
+ def execute
+ # no-op in FOSS
+ end
+ end
+end
+
+Preloaders::SingleHierarchyProjectGroupPlansPreloader.prepend_mod_with('Preloaders::SingleHierarchyProjectGroupPlansPreloader')
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 55f16aa3e3d..e6b1b33a82a 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -33,6 +33,11 @@ module Projects
SnippetsFinder.new(current_user, project: project).execute.select([:id, :title])
end
+ def contacts
+ Crm::ContactsFinder.new(current_user, group: project.group).execute
+ .select([:id, :email, :first_name, :last_name])
+ end
+
def labels_as_hash(target)
super(target, project_id: project.id, include_ancestor_groups: true)
end
diff --git a/config/feature_flags/development/group_project_api_preload_plans.yml b/config/feature_flags/development/group_project_api_preload_plans.yml
new file mode 100644
index 00000000000..3854bed461a
--- /dev/null
+++ b/config/feature_flags/development/group_project_api_preload_plans.yml
@@ -0,0 +1,8 @@
+---
+name: group_project_api_preload_plans
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77538
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350176
+milestone: '14.7'
+type: development
+group: group::authentication and authorization
+default_enabled: false
diff --git a/config/metrics/counts_all/20220122022215_web_ide_previews_success.yml b/config/metrics/counts_all/20220122022215_web_ide_previews_success.yml
new file mode 100644
index 00000000000..4042e892a61
--- /dev/null
+++ b/config/metrics/counts_all/20220122022215_web_ide_previews_success.yml
@@ -0,0 +1,21 @@
+---
+data_category: optional
+key_path: counts.web_ide_previews_success
+description: Count of Live Preview tab successful initializations in the Web IDE
+product_section: dev
+product_stage: create
+product_group: group::editor
+product_category: web_ide
+value_type: number
+status: active
+time_frame: all
+data_source: redis
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
+performance_indicator_type: []
+milestone: "14.8"
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 702ef64a2ca..50e958106d9 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -158,6 +158,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'milestones'
get 'commands'
get 'snippets'
+ get 'contacts'
end
end
@@ -580,6 +581,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
scope :service_ping, controller: :service_ping do
post :web_ide_clientside_preview # rubocop:todo Cop/PutProjectRoutesUnderScope
+ post :web_ide_clientside_preview_success # rubocop:todo Cop/PutProjectRoutesUnderScope
post :web_ide_pipelines_count # rubocop:todo Cop/PutProjectRoutesUnderScope
end
diff --git a/db/post_migrate/20220121214752_remove_projects_ci_stages_project_id_fk.rb b/db/post_migrate/20220121214752_remove_projects_ci_stages_project_id_fk.rb
new file mode 100644
index 00000000000..93f0b30571a
--- /dev/null
+++ b/db/post_migrate/20220121214752_remove_projects_ci_stages_project_id_fk.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class RemoveProjectsCiStagesProjectIdFk < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ return if Gitlab.com? # unsafe migration, skip on GitLab.com due to https://gitlab.com/groups/gitlab-org/-/epics/7249#note_819625526
+ return unless foreign_key_exists?(:ci_stages, :projects, name: "fk_2360681d1d")
+
+ with_lock_retries do
+ execute('LOCK projects, ci_stages IN ACCESS EXCLUSIVE MODE') if transaction_open?
+
+ remove_foreign_key_if_exists(:ci_stages, :projects, name: "fk_2360681d1d")
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(:ci_stages, :projects, name: "fk_2360681d1d", column: :project_id, target_column: :id, on_delete: :cascade)
+ end
+end
diff --git a/db/schema_migrations/20220121214752 b/db/schema_migrations/20220121214752
new file mode 100644
index 00000000000..84c03a6ed90
--- /dev/null
+++ b/db/schema_migrations/20220121214752
@@ -0,0 +1 @@
+c56983a489b56eb6b4a5cd5ae6947a322fd58b8e96bcdee1e38634d99727c432 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6f85319d656..c69bfd764bb 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -29261,9 +29261,6 @@ ALTER TABLE ONLY users_star_projects
ALTER TABLE ONLY alert_management_alerts
ADD CONSTRAINT fk_2358b75436 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE SET NULL;
-ALTER TABLE ONLY ci_stages
- ADD CONSTRAINT fk_2360681d1d FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY import_failures
ADD CONSTRAINT fk_24b824da43 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
diff --git a/doc/administration/feature_flags.md b/doc/administration/feature_flags.md
index afbf0759452..97875532f5d 100644
--- a/doc/administration/feature_flags.md
+++ b/doc/administration/feature_flags.md
@@ -130,16 +130,17 @@ irb(main):001:0> Feature.enable(:my_awesome_feature)
=> nil
```
-To check if a flag is enabled or disabled you can use `Feature.enabled?` or `Feature.disabled?`. For example, for a fictional feature flag named `my_awesome_feature`:
+When the feature is ready, GitLab removes the feature flag, and the option for
+enabling and disabling it no longer exists. The feature becomes available in all instances.
+
+### Check if a feature flag is enabled
+
+To check if a flag is enabled or disabled, use `Feature.enabled?` or `Feature.disabled?`.
+For example, for a feature flag named `my_awesome_feature` that is already enabled:
```ruby
-Feature.enable(:my_awesome_feature)
-=> nil
Feature.enabled?(:my_awesome_feature)
=> true
Feature.disabled?(:my_awesome_feature)
=> false
```
-
-When the feature is ready, GitLab removes the feature flag, and the option for
-enabling and disabling it no longer exists. The feature becomes available in all instances.
diff --git a/doc/ci/pipelines/img/merged_result_pipeline.png b/doc/ci/pipelines/img/merged_result_pipeline.png
deleted file mode 100644
index 2584cd4d38d..00000000000
--- a/doc/ci/pipelines/img/merged_result_pipeline.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/pipelines/merge_trains.md b/doc/ci/pipelines/merge_trains.md
index d47cbf5f47c..9819013dbe3 100644
--- a/doc/ci/pipelines/merge_trains.md
+++ b/doc/ci/pipelines/merge_trains.md
@@ -145,8 +145,22 @@ This is the fastest option to get the change merged into the target branch.
![Merge Immediately](img/merge_train_immediate_merge_v12_6.png)
WARNING:
-Each time you merge a merge request immediately, the current merge train
-is recreated and all pipelines restart.
+Each time you merge a merge request immediately, the current merge train is recreated,
+all pipelines restart, and [redundant pipelines are cancelled](#automatic-pipeline-cancellation).
+
+### Automatic pipeline cancellation
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12996) in GitLab 12.3.
+
+GitLab CI/CD can detect the presence of redundant pipelines, and cancels them
+to conserve CI resources.
+
+When a user merges a merge request immediately in an ongoing merge
+train, the train is reconstructed, because it recreates the expected
+post-merge commit and pipeline. In this case, the merge train may already
+have pipelines running against the previous expected post-merge commit.
+These pipelines are considered redundant and are automatically
+canceled.
## Troubleshooting
diff --git a/doc/ci/pipelines/pipelines_for_merged_results.md b/doc/ci/pipelines/pipelines_for_merged_results.md
index 91a49a48882..a7f467c65d7 100644
--- a/doc/ci/pipelines/pipelines_for_merged_results.md
+++ b/doc/ci/pipelines/pipelines_for_merged_results.md
@@ -6,128 +6,71 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Pipelines for merged results **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7380) in GitLab 11.10.
+A *pipeline for merged results* is a type of [pipeline for merge requests](merge_request_pipelines.md). It is a pipeline that runs against the results of the source and target branches merged together.
-When you submit a merge request, you are requesting to merge changes from a
-source branch into a target branch. By default, the CI pipeline runs jobs
-against the source branch.
+GitLab creates an internal commit with the merged results, so the pipeline can run
+against it. This commit does not exist in either branch,
+but you can view it in the pipeline details.
-With *pipelines for merged results*, the pipeline runs as if the changes from
-the source branch have already been merged into the target branch. The commit shown for the pipeline does not exist on the source or target branches but represents the combined target and source branches.
+The pipeline runs against the target branch as it exists at the moment you run the pipeline.
+Over time, while you're working in the source branch, the target branch might change.
+Any time you want to be sure the merged results are accurate, you should re-run the pipeline.
-![Merge request widget for merged results pipeline](img/merged_result_pipeline.png)
-
-If the pipeline fails due to a problem in the target branch, you can wait until the
-target is fixed and re-run the pipeline.
-This new pipeline runs as if the source is merged with the updated target, and you
-don't need to rebase.
-
-The pipeline does not automatically run when the target branch changes. Only changes
-to the source branch trigger a new pipeline. If a long time has passed since the last successful
-pipeline, you may want to re-run it before merge, to ensure that the source changes
-can still be successfully merged into the target.
-
-When the merge request can't be merged, the pipeline runs against the source branch only. For example, when:
+Pipelines for merged results can't run when:
- The target branch has changes that conflict with the changes in the source branch.
- The merge request is a [**Draft** merge request](../../user/project/merge_requests/drafts.md).
In these cases, the pipeline runs as a [pipeline for merge requests](merge_request_pipelines.md)
-and is labeled as `detached`. If these cases no longer exist, new pipelines
-again run against the merged results.
-
-Any user who has developer [permissions](../../user/permissions.md) can run a
-pipeline for merged results.
+and is labeled as `detached`.
## Prerequisites
-To enable pipelines for merge results:
+To use pipelines for merged results:
-- You must have the [Maintainer role](../../user/permissions.md).
-- You must be using [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or later.
-- You must not be using
- [fast forward merges](../../user/project/merge_requests/fast_forward_merge.md) yet.
- To follow progress, see [#26996](https://gitlab.com/gitlab-org/gitlab/-/issues/26996).
+- Your project's [CI/CD configuration file](../yaml/index.md) must be configured to
+ [run jobs in pipelines for merge requests](merge_request_pipelines.md#prerequisites).
- Your repository must be a GitLab repository, not an
[external repository](../ci_cd_for_external_repos/index.md).
+- You must not be using [fast forward merges](../../user/project/merge_requests/fast_forward_merge.md).
+ [An issue exits](https://gitlab.com/gitlab-org/gitlab/-/issues/26996) to change this behavior.
## Enable pipelines for merged results
-To enable pipelines for merged results for your project:
+To enable pipelines for merged results in a project, you must have at least the
+[Maintainer role](../../user/permissions.md):
-1. [Configure your CI/CD configuration file](merge_request_pipelines.md#prerequisites)
- so that the pipeline or individual jobs run for merge requests.
-1. Visit your project's **Settings > General** and expand **Merge requests**.
-1. Check **Enable merged results pipelines**.
-1. Click **Save changes**.
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > General**.
+1. Expand **Merge requests**.
+1. Select **Enable merged results pipelines**.
+1. Select **Save changes**.
WARNING:
-If you select the checkbox but don't configure your CI/CD to use
+If you select the checkbox but don't configure your pipeline to use
pipelines for merge requests, your merge requests may become stuck in an
unresolved state or your pipelines may be dropped.
-## Using Merge Trains
-
-When you enable [Pipelines for merged results](#pipelines-for-merged-results),
-GitLab [automatically displays](merge_trains.md#add-a-merge-request-to-a-merge-train)
-a **Start/Add Merge Train button**.
-
-Generally, this is a safer option than merging merge requests immediately, because your
-merge request is evaluated with an expected post-merge result before the actual
-merge happens.
-
-For more information, read the [documentation on Merge Trains](merge_trains.md).
-
-## Automatic pipeline cancellation
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12996) in GitLab 12.3.
-
-GitLab CI/CD can detect the presence of redundant pipelines, and cancels them
-to conserve CI resources.
-
-When a user merges a merge request immediately in an ongoing merge
-train, the train is reconstructed, because it recreates the expected
-post-merge commit and pipeline. In this case, the merge train may already
-have pipelines running against the previous expected post-merge commit.
-These pipelines are considered redundant and are automatically
-canceled.
-
## Troubleshooting
-### Pipelines for merged results not created even with new change pushed to merge request
-
-Can be caused by some disabled feature flags. Please make sure that
-the following feature flags are enabled on your GitLab instance:
-
-- `:merge_ref_auto_sync`
-
-To check and set these feature flag values, please ask an administrator to:
-
-1. Log into the Rails console of the GitLab instance:
-
- ```shell
- sudo gitlab-rails console
- ```
-
-1. Check if the flags are enabled or not:
-
- ```ruby
- Feature.enabled?(:merge_ref_auto_sync)
- ```
+### Pipelines for merged results are not created
-1. If needed, enable the feature flags:
+In GitLab 13.7 and earlier, pipelines for merged results might not be created due
+to a disabled [feature flag](../../user/feature_flags.md). This feature flag
+[was removed](https://gitlab.com/gitlab-org/gitlab/-/issues/299115) in GitLab 13.8.
+Upgrade to 13.8 or later, or make sure the `:merge_ref_auto_sync`
+[feature flag is enabled](../../administration/feature_flags.md#check-if-a-feature-flag-is-enabled)
+on your GitLab instance.
- ```ruby
- Feature.enable(:merge_ref_auto_sync)
- ```
+### Pipelines fail intermittently with a `fatal: reference is not a tree:` error
-### Intermittently pipelines fail by `fatal: reference is not a tree:` error
+Pipelines for merged results run on a merge ref for a merge request
+(`refs/merge-requests/<iid>/merge`), so the Git reference could be overwritten at an
+unexpected time.
-Since pipelines for merged results are a run on a merge ref of a merge request
-(`refs/merge-requests/<iid>/merge`), the Git reference could be overwritten at an
-unexpected timing. For example, when a source or target branch is advanced.
-In this case, the pipeline fails because of `fatal: reference is not a tree:` error,
-which indicates that the checkout-SHA is not found in the merge ref.
+For example, when a source or target branch is advanced, the pipeline fails with
+the `fatal: reference is not a tree:` error, which indicates that the checkout-SHA
+is not found in the merge ref.
-This behavior was improved at GitLab 12.4 by introducing [Persistent pipeline refs](../troubleshooting.md#fatal-reference-is-not-a-tree-error).
-You should be able to create pipelines at any timings without concerning the error.
+This behavior was improved in GitLab 12.4 by introducing [persistent pipeline refs](../troubleshooting.md#fatal-reference-is-not-a-tree-error).
+Upgrade to GitLab 12.4 or later to resolve the problem.
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index 7465f079ce7..ad4ef41425d 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -174,7 +174,7 @@ a branch to its remote repository. To illustrate the problem, suppose you've had
This occurs because the previous pipeline cannot find a checkout-SHA (which is associated with the pipeline record)
from the `example` branch that the commit history has already been overwritten by the force-push.
Similarly, [Pipelines for merged results](pipelines/pipelines_for_merged_results.md)
-might have failed intermittently due to [the same reason](pipelines/pipelines_for_merged_results.md#intermittently-pipelines-fail-by-fatal-reference-is-not-a-tree-error).
+might have failed intermittently due to [the same reason](pipelines/pipelines_for_merged_results.md#pipelines-fail-intermittently-with-a-fatal-reference-is-not-a-tree-error).
As of GitLab 12.4, we've improved this behavior by persisting pipeline refs exclusively.
To illustrate its life cycle:
diff --git a/doc/user/packages/container_registry/reduce_container_registry_storage.md b/doc/user/packages/container_registry/reduce_container_registry_storage.md
index e2242a85b75..b8dfa0092c3 100644
--- a/doc/user/packages/container_registry/reduce_container_registry_storage.md
+++ b/doc/user/packages/container_registry/reduce_container_registry_storage.md
@@ -258,7 +258,7 @@ being cleaned up is minimal.
- [Delete registry repository](../../../api/container_registry.md#delete-registry-repository)
- [Delete a registry repository tag](../../../api/container_registry.md#delete-a-registry-repository-tag)
- [Delete registry repository tags in bulk](../../../api/container_registry.md#delete-registry-repository-tags-in-bulk)
-- [Delete a package](../package_registry/index.md#delete-a-package)
+- [Delete a package](../package_registry/reduce_package_registry_storage.md#delete-a-package)
## Troubleshooting cleanup policies
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index 52f5a1fcc0d..97cf4419bb8 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -210,65 +210,8 @@ from the GitLab server.
## Reduce storage usage
-Blobs are kept forever on the GitLab server, and there is no hard limit on how much data can be
-stored.
-
-### Using the API to clear the cache
-
-To reclaim disk space used by image blobs that are no longer needed, use
-the [Dependency Proxy API](../../../api/dependency_proxy.md) to clear the entire
-cache.
-
-If you clear the cache, the next time a pipeline runs it must pull an image or tag from Docker Hub.
-
-### Cleanup policies
-
-#### Enable cleanup policies from within GitLab
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340777) in GitLab 14.6
-
-You can enable an automatic time-to-live (TTL) policy for the Dependency Proxy from the user
-interface. To do this, navigate to your group's **Settings > Packages & Registries > Dependency Proxy**
-and enable the setting to automatically clear items from the cache after 90 days.
-
-#### Enable cleanup policies with GraphQL
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/294187) in GitLab 14.4.
-
-The cleanup policy is a scheduled job you can use to clear cached images that are no longer used,
-freeing up additional storage space. The policies use time-to-live (TTL) logic:
-
-- The number of days is configured.
-- All cached dependency proxy files that have not been pulled in that many days are deleted.
-
-Use the [GraphQL API](../../../api/graphql/reference/index.md#mutationupdatedependencyproxyimagettlgrouppolicy)
-to enable and configure cleanup policies:
-
-```graphql
-mutation {
- updateDependencyProxyImageTtlGroupPolicy(input:
- {
- groupPath: "<your-full-group-path>",
- enabled: true,
- ttl: 90
- }
- ) {
- dependencyProxyImageTtlPolicy {
- enabled
- ttl
- }
- errors
- }
-}
-```
-
-See the [Getting started with GraphQL](../../../api/graphql/getting_started.md)
-guide to learn how to make GraphQL queries.
-
-When the policy is initially enabled, the default TTL setting is 90 days. Once enabled, stale
-dependency proxy files are queued for deletion each day. Deletion may not occur right away due to
-processing time. If the image is pulled after the cached files are marked as expired, the expired
-files are ignored and new files are downloaded and cached from the external registry.
+For information on reducing your storage use on the Dependency Proxy, see
+[Reduce Dependency Proxy storage use](reduce_dependency_proxy_storage.md).
## Docker Hub rate limits and the Dependency Proxy
diff --git a/doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md b/doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md
new file mode 100644
index 00000000000..cd04d2e696b
--- /dev/null
+++ b/doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md
@@ -0,0 +1,74 @@
+---
+stage: Package
+group: Package
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Reduce Dependency Proxy Storage **(FREE)**
+
+There's no automatic removal process for blobs. Unless you delete them manually, they're stored
+indefinitely. Since this impacts your
+[storage usage quota](../../usage_quotas.md),
+it's important that you clear unused items from the cache. This page covers several options for
+doing so.
+
+## Check Dependency Proxy Storage Use
+
+The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages, which includes the Dependency Proxy,
+however, the storage is not yet displayed.
+
+## Use the API to clear the cache
+
+To reclaim disk space used by image blobs that are no longer needed, use the
+[Dependency Proxy API](../../../api/dependency_proxy.md)
+to clear the entire cache. If you clear the cache, the next time a pipeline runs it must pull an
+image or tag from Docker Hub.
+
+## Cleanup policies
+
+### Enable cleanup policies from within GitLab
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340777) in GitLab 14.6
+
+You can enable an automatic time-to-live (TTL) policy for the Dependency Proxy from the user
+interface. To do this, navigate to your group's **Settings > Packages & Registries > Dependency Proxy**
+and enable the setting to automatically clear items from the cache after 90 days.
+
+### Enable cleanup policies with GraphQL
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/294187) in GitLab 14.4.
+
+The cleanup policy is a scheduled job you can use to clear cached images that are no longer used,
+freeing up additional storage space. The policies use time-to-live (TTL) logic:
+
+- The number of days is configured.
+- All cached dependency proxy files that have not been pulled in that many days are deleted.
+
+Use the [GraphQL API](../../../api/graphql/reference/index.md#mutationupdatedependencyproxyimagettlgrouppolicy)
+to enable and configure cleanup policies:
+
+```graphql
+mutation {
+ updateDependencyProxyImageTtlGroupPolicy(input:
+ {
+ groupPath: "<your-full-group-path>",
+ enabled: true,
+ ttl: 90
+ }
+ ) {
+ dependencyProxyImageTtlPolicy {
+ enabled
+ ttl
+ }
+ errors
+ }
+}
+```
+
+See the [Getting started with GraphQL](../../../api/graphql/getting_started.md)
+guide to learn how to make GraphQL queries.
+
+When the policy is initially enabled, the default TTL setting is 90 days. Once enabled, stale
+dependency proxy files are queued for deletion each day. Deletion may not occur right away due to
+processing time. If the image is pulled after the cached files are marked as expired, the expired
+files are ignored and new files are downloaded and cached from the external registry.
diff --git a/doc/user/packages/package_registry/index.md b/doc/user/packages/package_registry/index.md
index 3311b271126..73dc15b8a92 100644
--- a/doc/user/packages/package_registry/index.md
+++ b/doc/user/packages/package_registry/index.md
@@ -12,6 +12,15 @@ With the GitLab Package Registry, you can use GitLab as a private or public regi
of [supported package managers](#supported-package-managers).
You can publish and share packages, which can be consumed as a dependency in downstream projects.
+## Package workflows
+
+Learn how to use the GitLab Package Registry to build your own custom package workflow:
+
+- [Use a project as a package registry](../workflows/project_registry.md)
+ to publish all of your packages to one project.
+
+- Publish multiple different packages from one [monorepo project](../workflows/working_with_monorepos.md).
+
## View packages
You can view packages for your project or group.
@@ -77,46 +86,10 @@ when you view the package details:
You can view which pipeline published the package, and the commit and user who triggered it. However, the history is limited to five updates of a given package.
-## Download a package
+## Reduce storage usage
-To download a package:
-
-1. Go to **Packages & Registries > Package Registry**.
-1. Select the name of the package you want to download.
-1. In the **Activity** section, select the name of the package you want to download.
-
-## Delete a package
-
-You cannot edit a package after you publish it in the Package Registry. Instead, you
-must delete and recreate it.
-
-To delete a package, you must have suitable [permissions](../../permissions.md).
-
-You can delete packages by using [the API](../../../api/packages.md#delete-a-project-package) or the UI.
-
-To delete a package in the UI, from your group or project:
-
-1. Go to **Packages & Registries > Package Registry**.
-1. Find the name of the package you want to delete.
-1. Click **Delete**.
-
-The package is permanently deleted.
-
-## Delete files associated with a package
-
-To delete package files, you must have suitable [permissions](../../permissions.md).
-
-You can delete packages by using [the API](../../../api/packages.md#delete-a-package-file) or the UI.
-
-To delete package files in the UI, from your group or project:
-
-1. Go to **Packages & Registries > Package Registry**.
-1. Find the name of the package you want to delete.
-1. Select the package to view additional details.
-1. Find the name of the file you would like to delete.
-1. Expand the ellipsis and select **Delete file**.
-
-The package files are permanently deleted.
+For information on reducing your storage use for the Package Registry, see
+[Reduce Dependency Proxy storage use](reduce_package_registry_storage.md).
## Disable the Package Registry
@@ -135,15 +108,6 @@ You can also remove the Package Registry for your project specifically:
The **Packages & Registries > Package Registry** entry is removed from the sidebar.
-## Package workflows
-
-Learn how to use the GitLab Package Registry to build your own custom package workflow:
-
-- [Use a project as a package registry](../workflows/project_registry.md)
- to publish all of your packages to one project.
-
-- Publish multiple different packages from one [monorepo project](../workflows/working_with_monorepos.md).
-
## Supported package managers
WARNING:
diff --git a/doc/user/packages/package_registry/reduce_package_registry_storage.md b/doc/user/packages/package_registry/reduce_package_registry_storage.md
new file mode 100644
index 00000000000..c2e4cd8d889
--- /dev/null
+++ b/doc/user/packages/package_registry/reduce_package_registry_storage.md
@@ -0,0 +1,52 @@
+---
+stage: Package
+group: Package
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Reduce Package Registry Storage **(FREE)**
+
+Without cleanup, package registries become large over time. When a large number of packages and
+their files are added:
+
+- Fetching the list of packages becomes slower.
+- They take up a large amount of storage space on the server, impacting your [storage usage quota](../../usage_quotas.md).
+
+We recommend deleting unnecessary packages and files. This page offers examples of how to do so.
+
+## Check Package Registry Storage Use
+
+The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages.
+
+## Delete a package
+
+You cannot edit a package after you publish it in the Package Registry. Instead, you
+must delete and recreate it.
+
+To delete a package, you must have suitable [permissions](../../permissions.md).
+
+You can delete packages by using [the API](../../../api/packages.md#delete-a-project-package) or the UI.
+
+To delete a package in the UI, from your group or project:
+
+1. Go to **Packages & Registries > Package Registry**.
+1. Find the name of the package you want to delete.
+1. Click **Delete**.
+
+The package is permanently deleted.
+
+## Delete files associated with a package
+
+To delete package files, you must have suitable [permissions](../../permissions.md).
+
+You can delete packages by using [the API](../../../api/packages.md#delete-a-package-file) or the UI.
+
+To delete package files in the UI, from your group or project:
+
+1. Go to **Packages & Registries > Package Registry**.
+1. Find the name of the package you want to delete.
+1. Select the package to view additional details.
+1. Find the name of the file you would like to delete.
+1. Expand the ellipsis and select **Delete file**.
+
+The package files are permanently deleted.
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 6c18fc158f5..6f23495b513 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -18,7 +18,7 @@ When a branch is protected, the default behavior enforces these restrictions on
| Action | Who can do it |
|:-------------------------|:------------------------------------------------------------------|
-| Protect a branch | Maintainers only. |
+| Protect a branch | At least the Maintainer role. |
| Push to the branch | GitLab administrators and anyone with **Allowed** permission. (1) |
| Force push to the branch | No one. |
| Delete the branch | No one. |
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index d3d1f03585b..a40bd1f58d4 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -84,10 +84,11 @@ module API
paginate(projects)
end
- def present_projects(params, projects)
+ def present_projects(params, projects, single_hierarchy: false)
options = {
with: params[:simple] ? Entities::BasicProjectDetails : Entities::Project,
- current_user: current_user
+ current_user: current_user,
+ single_hierarchy: single_hierarchy
}
projects, options = with_custom_attributes(projects, options)
@@ -306,7 +307,7 @@ module API
projects = find_group_projects(params, finder_options)
- present_projects(params, projects)
+ present_projects(params, projects, single_hierarchy: true)
end
desc 'Get a list of shared projects in this group' do
diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb
index a4bd06aec10..aabecb43653 100644
--- a/lib/api/projects_relation_builder.rb
+++ b/lib/api/projects_relation_builder.rb
@@ -13,6 +13,7 @@ module API
preload_repository_cache(projects_relation)
Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_relation, options[:current_user]).execute if options[:current_user]
+ Preloaders::SingleHierarchyProjectGroupPlansPreloader.new(projects_relation).execute if options[:single_hierarchy]
projects_relation
end
diff --git a/lib/gitlab/database/gitlab_loose_foreign_keys.yml b/lib/gitlab/database/gitlab_loose_foreign_keys.yml
index e3793c14ee2..3051bd9ed61 100644
--- a/lib/gitlab/database/gitlab_loose_foreign_keys.yml
+++ b/lib/gitlab/database/gitlab_loose_foreign_keys.yml
@@ -191,6 +191,10 @@ ci_sources_pipelines:
- table: projects
column: project_id
on_delete: async_delete
+ci_stages:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
ci_triggers:
- table: users
column: owner_id
diff --git a/lib/gitlab/usage_data_counters/web_ide_counter.rb b/lib/gitlab/usage_data_counters/web_ide_counter.rb
index 9f2f4ac3971..f2753c8f215 100644
--- a/lib/gitlab/usage_data_counters/web_ide_counter.rb
+++ b/lib/gitlab/usage_data_counters/web_ide_counter.rb
@@ -3,7 +3,7 @@
module Gitlab
module UsageDataCounters
class WebIdeCounter < BaseCounter
- KNOWN_EVENTS = %w[commits views merge_requests previews terminals pipelines].freeze
+ KNOWN_EVENTS = %w[commits views merge_requests previews previews_success terminals pipelines].freeze
PREFIX = 'web_ide'
class << self
@@ -33,6 +33,12 @@ module Gitlab
count('previews')
end
+ def increment_previews_success_count
+ return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
+
+ count('previews_success')
+ end
+
private
def redis_key(event)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6a42992d50c..0705ef009ce 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5250,6 +5250,9 @@ msgstr ""
msgid "Average per day: %{average}"
msgstr ""
+msgid "Awaiting user signup"
+msgstr ""
+
msgid "Award added"
msgstr ""
diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
index 865b31a28d7..79edc261809 100644
--- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb
+++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Projects::AutocompleteSourcesController do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group, reload: true) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -69,4 +69,62 @@ RSpec.describe Projects::AutocompleteSourcesController do
end
end
end
+
+ describe 'GET contacts' do
+ let_it_be(:contact_1) { create(:contact, group: group) }
+ let_it_be(:contact_2) { create(:contact, group: group) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when feature flag is enabled' do
+ context 'when a group has contact relations enabled' do
+ before do
+ create(:crm_settings, group: group, enabled: true)
+ end
+
+ context 'when a user can read contacts' do
+ it 'lists contacts' do
+ group.add_developer(user)
+
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ emails = json_response.map { |contact_data| contact_data["email"] }
+ expect(emails).to match_array([contact_1.email, contact_2.email])
+ end
+ end
+
+ context 'when a user can not read contacts' do
+ it 'renders 404' do
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when a group has contact relations disabled' do
+ it 'renders 404' do
+ group.add_developer(user)
+
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it 'renders 404' do
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/service_ping_controller_spec.rb b/spec/controllers/projects/service_ping_controller_spec.rb
index 729488cc86e..13b34290962 100644
--- a/spec/controllers/projects/service_ping_controller_spec.rb
+++ b/spec/controllers/projects/service_ping_controller_spec.rb
@@ -69,6 +69,33 @@ RSpec.describe Projects::ServicePingController do
end
end
+ describe 'POST #web_ide_clientside_preview_success' do
+ subject { post :web_ide_clientside_preview_success, params: { namespace_id: project.namespace, project_id: project } }
+
+ context 'when web ide clientside preview is enabled' do
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: true)
+ end
+
+ it_behaves_like 'counter is not increased'
+ it_behaves_like 'counter is increased', 'WEB_IDE_PREVIEWS_SUCCESS_COUNT'
+ end
+
+ context 'when web ide clientside preview is not enabled' do
+ let(:user) { project.owner }
+
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: false)
+ end
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'POST #web_ide_pipelines_count' do
subject { post :web_ide_pipelines_count, params: { namespace_id: project.namespace, project_id: project } }
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index d25cddea902..2ca4ff94911 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -583,7 +583,11 @@ RSpec.describe 'Project issue boards', :js do
end
page.within(find('.js-board-settings-sidebar')) do
- accept_confirm { find('[data-testid="remove-list"]').click }
+ click_button 'Remove list'
+ end
+
+ page.within('.modal') do
+ click_button 'Remove list'
end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index b0e4729db8b..a88eca5cbcc 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -707,9 +707,10 @@ RSpec.describe 'GFM autocomplete', :js do
def start_and_cancel_discussion
fill_in('Reply to comment', with: 'Whoops!')
+ click_button('Cancel')
- page.accept_alert 'Are you sure you want to cancel creating this comment?' do
- click_button('Cancel')
+ page.within('.modal') do
+ click_button('OK', match: :first)
end
wait_for_requests
diff --git a/spec/finders/crm/contacts_finder_spec.rb b/spec/finders/crm/contacts_finder_spec.rb
new file mode 100644
index 00000000000..151af1ad825
--- /dev/null
+++ b/spec/finders/crm/contacts_finder_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Crm::ContactsFinder do
+ let_it_be(:user) { create(:user) }
+
+ describe '#execute' do
+ subject { described_class.new(user, group: group).execute }
+
+ context 'when customer relations feature is enabled for the group' do
+ let_it_be(:root_group) { create(:group, :crm_enabled) }
+ let_it_be(:group) { create(:group, parent: root_group) }
+
+ let_it_be(:contact_1) { create(:contact, group: root_group) }
+ let_it_be(:contact_2) { create(:contact, group: root_group) }
+
+ context 'when user does not have permissions to see contacts in the group' do
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when user is member of the root group' do
+ before do
+ root_group.add_developer(user)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ it 'returns all group contacts' do
+ expect(subject).to match_array([contact_1, contact_2])
+ end
+ end
+ end
+
+ context 'when user is member of the sub group' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ context 'when customer relations feature is disabled for the group' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:contact) { create(:contact, group: group) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 842c8fd131e..7f40c426b30 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -1,8 +1,9 @@
-import { GlDrawer, GlLabel } from '@gitlab/ui';
+import { GlDrawer, GlLabel, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
@@ -20,8 +21,7 @@ describe('BoardSettingsSidebar', () => {
const labelTitle = mockLabelList.label.title;
const labelColor = mockLabelList.label.color;
const listId = mockLabelList.id;
-
- const findRemoveButton = () => wrapper.findByTestId('remove-list');
+ const modalID = 'board-settings-sidebar-modal';
const createComponent = ({
canAdminList = false,
@@ -46,6 +46,9 @@ describe('BoardSettingsSidebar', () => {
canAdminList,
scopedLabelsAvailable: false,
},
+ directives: {
+ GlModal: createMockDirective(),
+ },
stubs: {
GlDrawer: stubComponent(GlDrawer, {
template: '<div><slot name="header"></slot><slot></slot></div>',
@@ -56,6 +59,8 @@ describe('BoardSettingsSidebar', () => {
};
const findLabel = () => wrapper.find(GlLabel);
const findDrawer = () => wrapper.find(GlDrawer);
+ const findModal = () => wrapper.find(GlModal);
+ const findRemoveButton = () => wrapper.find(GlButton);
afterEach(() => {
jest.restoreAllMocks();
@@ -161,5 +166,16 @@ describe('BoardSettingsSidebar', () => {
expect(findRemoveButton().exists()).toBe(true);
});
+
+ it('has the correct ID on the button', () => {
+ createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
+ const binding = getBinding(findRemoveButton().element, 'gl-modal');
+ expect(binding.value).toBe(modalID);
+ });
+
+ it('has the correct ID on the modal', () => {
+ createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
+ expect(findModal().props('modalId')).toBe(modalID);
+ });
});
});
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 0c0ddcbbfd3..d6df061ad04 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -1,9 +1,11 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
+import { dispatch } from 'codesandbox-api';
import smooshpack from 'smooshpack';
import Vuex from 'vuex';
import Clientside from '~/ide/components/preview/clientside.vue';
+import { PING_USAGE_PREVIEW_KEY, PING_USAGE_PREVIEW_SUCCESS_KEY } from '~/ide/constants';
import eventHub from '~/ide/eventhub';
jest.mock('smooshpack', () => ({
@@ -39,6 +41,7 @@ describe('IDE clientside preview', () => {
const storeClientsideActions = {
pingUsage: jest.fn().mockReturnValue(Promise.resolve({})),
};
+ const dispatchCodesandboxReady = () => dispatch({ type: 'done' });
const waitForCalls = () => new Promise(setImmediate);
@@ -110,6 +113,20 @@ describe('IDE clientside preview', () => {
it('pings usage', () => {
expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(1);
+ expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
+ expect.anything(),
+ PING_USAGE_PREVIEW_KEY,
+ );
+ });
+
+ it('pings usage success', async () => {
+ dispatchCodesandboxReady();
+ await wrapper.vm.$nextTick();
+ expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(2);
+ expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
+ expect.anything(),
+ PING_USAGE_PREVIEW_SUCCESS_KEY,
+ );
});
});
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
index 88d7a630a90..d2777623b0d 100644
--- a/spec/frontend/ide/stores/modules/clientside/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
@@ -1,11 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { PING_USAGE_PREVIEW_KEY } from '~/ide/constants';
import * as actions from '~/ide/stores/modules/clientside/actions';
import axios from '~/lib/utils/axios_utils';
const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
-const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/web_ide_clientside_preview`;
+const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/${PING_USAGE_PREVIEW_KEY}`;
describe('IDE store module clientside actions', () => {
let rootGetters;
@@ -30,7 +31,7 @@ describe('IDE store module clientside actions', () => {
mock.onPost(TEST_USAGE_URL).reply(() => usageSpy());
- testAction(actions.pingUsage, null, rootGetters, [], [], () => {
+ testAction(actions.pingUsage, PING_USAGE_PREVIEW_KEY, rootGetters, [], [], () => {
expect(usageSpy).toHaveBeenCalled();
done();
});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index b559afb4512..b2756e506eb 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -16,6 +16,7 @@ import {
MEMBER_STATE_AWAITING,
MEMBER_STATE_ACTIVE,
USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_AWAITING_USER_SIGNUP,
BADGE_LABELS_PENDING_OWNER_APPROVAL,
TAB_QUERY_PARAM_VALUES,
} from '~/members/constants';
@@ -131,9 +132,9 @@ describe('MembersTable', () => {
describe('Invited column', () => {
describe.each`
state | userState | expectedBadgeLabel
- ${MEMBER_STATE_CREATED} | ${null} | ${''}
+ ${MEMBER_STATE_CREATED} | ${null} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
- ${MEMBER_STATE_AWAITING} | ${''} | ${''}
+ ${MEMBER_STATE_AWAITING} | ${''} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
${MEMBER_STATE_ACTIVE} | ${null} | ${''}
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 2b6f22e68f1..b91348eb408 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -362,4 +362,11 @@ RSpec.describe Ci::Stage, :models do
end
it_behaves_like 'manual playable stage', :ci_stage_entity
+
+ context 'loose foreign key on ci_stages.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_stage_entity, project: parent) }
+ end
+ end
end
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 3bf592ed2b9..30c5d17b2aa 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe BlobPresenter do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:repository) { project.repository }
let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') }
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 6a02f81fcae..df9be2616c5 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe API::API do
describe 'logging', :aggregate_failures do
let_it_be(:project) { create(:project, :public) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
context 'when the endpoint is handled by the application' do
context 'when the endpoint supports all possible fields' do
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 13838cffd76..1b87a5e24f5 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -988,7 +988,7 @@ RSpec.describe API::Ci::Pipelines do
describe 'DELETE /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
- let(:owner) { project.owner }
+ let(:owner) { project.first_owner }
it 'destroys the pipeline' do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
index df64c0bd22b..e1dc347f8dd 100644
--- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb
+++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe API::Ci::Runners do
end
include_context 'when authorized', 'project' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
def get_token
project.reload.runners_token
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index 578a71a7272..c19defa37e8 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'Getting Ci Cd Setting' do
include GraphqlHelpers
let_it_be_with_reload(:project) { create(:project, :repository) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let(:fields) do
<<~QUERY
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
index 802ab847b3d..35a70a180a2 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'container repository details' do
)
end
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:variables) { {} }
let(:tags) { %w[latest tag1 tag2 tag3 tag4 tag5] }
let(:container_repository_global_id) { container_repository.to_global_id.to_s }
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index 8bbeae97f57..e80f5e0e0ff 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -166,7 +166,7 @@ RSpec.describe 'GitlabSchema configurations' do
end
context 'authentication' do
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
it 'authenticates all queries' do
subject
@@ -216,7 +216,7 @@ RSpec.describe 'GitlabSchema configurations' do
context "global id's" do
it 'uses GlobalID to expose ids' do
post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)),
- current_user: project.owner)
+ current_user: project.first_owner)
parsed_id = GlobalID.parse(graphql_data['project']['id'])
diff --git a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
index 05f6804a208..30e7f196542 100644
--- a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe 'CiCdSettingsUpdate' do
end
context 'when authorized' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
it 'updates ci cd settings' do
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index b53a7ddde32..5269c60b50a 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe 'CiJobTokenScopeAddProject' do
end
context 'when authorized' do
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
before do
target_project.add_developer(current_user)
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
index f1f42b00ada..b62291d1ebd 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'CiJobTokenScopeRemoveProject' do
end
context 'when authorized' do
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
before do
target_project.add_guest(current_user)
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
index 08959d354e2..37656ab4eea 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'PipelineDestroy' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, user: user) }
let(:mutation) do
diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
index 322706be119..12368e7e9c5 100644
--- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe 'RunnersRegistrationTokenReset' do
end
include_context 'when authorized', 'project' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
def get_token
project.reload.runners_token
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
index 929609d4160..0c034f38dc8 100644
--- a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'ConfigureSastIac' do
let(:mutation_response) { graphql_mutation_response(:configureSastIac) }
context 'when authorized' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
it 'creates a branch with sast iac configured' do
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
index 23a154b71a0..8fa6e44b208 100644
--- a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'ConfigureSecretDetection' do
let(:mutation_response) { graphql_mutation_response(:configureSecretDetection) }
context 'when authorized' do
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
it 'creates a branch with secret detection configured' do
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
index dc16847a669..e3ea9e46353 100644
--- a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
+++ b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting a repository in a project' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
let(:fields) do
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 692143b2215..bbab6012f3f 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'getting container repositories in a project' do
)
end
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:variables) { {} }
let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') }
let(:container_repositories_count_response) { graphql_data.dig('project', 'containerRepositoriesCount') }
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
index 40a3281d3b7..2b85704f479 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'getting a detailed sentry error' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:sentry_detailed_error) { build(:error_tracking_sentry_detailed_error) }
let(:sentry_gid) { sentry_detailed_error.to_global_id.to_s }
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
index a540386a9de..3ca0e35882a 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'sentry errors requests' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let(:query) do
graphql_query_for(
diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb
index 9b24698f40c..e7534945e7a 100644
--- a/spec/requests/api/graphql/project/grafana_integration_spec.rb
+++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'Getting Grafana Integration' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
let(:fields) do
diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
index 9d98498ca8a..46fd65db1c5 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Getting versions related to an issue' do
create(:design_version, issue: issue)
end
- let_it_be(:owner) { issue.project.owner }
+ let_it_be(:owner) { issue.project.first_owner }
def version_query(params = version_params)
query_graphql_field(:versions, params, version_query_fields)
diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
index def41efddde..f0205319983 100644
--- a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Getting designs related to an issue' do
include DesignManagementTestHelpers
let_it_be(:design) { create(:design, :with_smaller_image_versions, versions_count: 1) }
- let_it_be(:current_user) { design.project.owner }
+ let_it_be(:current_user) { design.project.first_owner }
let(:design_query) do
<<~NODE
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
index 7148750b6cb..de2ace95757 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Getting designs related to an issue' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, :with_file, versions_count: 1, issue: issue) }
- let_it_be(:current_user) { project.owner }
+ let_it_be(:current_user) { project.first_owner }
let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project) }
before do
diff --git a/spec/requests/api/graphql/project/repository/blobs_spec.rb b/spec/requests/api/graphql/project/repository/blobs_spec.rb
index 12f6fbd793e..ba87f1100f2 100644
--- a/spec/requests/api/graphql/project/repository/blobs_spec.rb
+++ b/spec/requests/api/graphql/project/repository/blobs_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting blobs in a project repository' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:paths) { ["CONTRIBUTING.md", "README.md"] }
let(:ref) { project.default_branch }
let(:fields) do
diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb
index 8810f2fa3d5..b00f64c3db6 100644
--- a/spec/requests/api/graphql/project/repository_spec.rb
+++ b/spec/requests/api/graphql/project/repository_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting a repository in a project' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('repository'.classify)}
diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb
index f4cd316da96..25e878a5b1a 100644
--- a/spec/requests/api/graphql/project/tree/tree_spec.rb
+++ b/spec/requests/api/graphql/project/tree/tree_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'getting a tree in a project' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:path) { "" }
let(:ref) { "master" }
let(:fields) do
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
index faf671d350f..0488bce4663 100644
--- a/spec/requests/api/markdown_spec.rb
+++ b/spec/requests/api/markdown_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe API::Markdown do
end
context "when authorized" do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it_behaves_like "rendered markdown text without GFM"
end
@@ -97,7 +97,7 @@ RSpec.describe API::Markdown do
context "with project" do
let(:params) { { text: text, gfm: true, project: project.full_path } }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it "renders markdown text" do
expect(response).to have_gitlab_http_status(:created)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 0a90be4a068..2a266ff5d19 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -2896,7 +2896,7 @@ RSpec.describe API::MergeRequests do
it 'is false for an unauthorized user' do
expect do
- put api("/projects/#{target_project.id}/merge_requests/#{merge_request.iid}", target_project.owner), params: { state_event: 'close', remove_source_branch: true }
+ put api("/projects/#{target_project.id}/merge_requests/#{merge_request.iid}", target_project.first_owner), params: { state_event: 'close', remove_source_branch: true }
end.not_to change { merge_request.reload.merge_params }
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index b9c458373a8..2bc31153f2c 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -450,7 +450,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
expect_next_instance_of(Projects::ImportExport::ExportService) do |service|
expect(service).to receive(:execute)
end
- post api(path, project.owner), params: params
+ post api(path, project.first_owner), params: params
expect(response).to have_gitlab_http_status(:accepted)
end
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
index 33c86d56ed4..bf78ff56206 100644
--- a/spec/requests/api/project_snapshots_spec.rb
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe API::ProjectSnapshots do
end
it 'returns authentication error as project owner' do
- get api("/projects/#{project.id}/snapshot", project.owner)
+ get api("/projects/#{project.id}/snapshot", project.first_owner)
expect(response).to have_gitlab_http_status(:forbidden)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index bf41a808219..73c78b85b3b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -30,7 +30,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do
context 'when the languages were detected before' do
before do
- Projects::DetectRepositoryLanguagesService.new(project, project.owner).execute
+ Projects::DetectRepositoryLanguagesService.new(project, project.first_owner).execute
end
it 'returns the detection from the database' do
@@ -2710,7 +2710,7 @@ RSpec.describe API::Projects do
it 'returns the project users' do
get api("/projects/#{project.id}/users", current_user)
- user = project.namespace.owner
+ user = project.namespace.first_owner
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
diff --git a/spec/requests/boards/lists_controller_spec.rb b/spec/requests/boards/lists_controller_spec.rb
index 4d9f1dace4d..47f4925d5b0 100644
--- a/spec/requests/boards/lists_controller_spec.rb
+++ b/spec/requests/boards/lists_controller_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Boards::ListsController do
describe '#index' do
let(:board) { create(:board) }
- let(:user) { board.project.owner }
+ let(:user) { board.project.first_owner }
it 'does not have N+1 queries' do
login_as(user)
diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb
index 58843a7fec4..eed035608d0 100644
--- a/spec/requests/import/gitlab_projects_controller_spec.rb
+++ b/spec/requests/import/gitlab_projects_controller_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Import::GitlabProjectsController do
include_context 'workhorse headers'
let_it_be(:namespace) { create(:namespace) }
- let_it_be(:user) { namespace.owner }
+ let_it_be(:user) { namespace.first_owner }
before do
login_as(user)
diff --git a/spec/requests/projects/clusters/integrations_controller_spec.rb b/spec/requests/projects/clusters/integrations_controller_spec.rb
index 323c61b9af3..c05e3da675c 100644
--- a/spec/requests/projects/clusters/integrations_controller_spec.rb
+++ b/spec/requests/projects/clusters/integrations_controller_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Projects::Clusters::IntegrationsController do
describe 'POST create_or_update' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it_behaves_like '#create_or_update action' do
let(:path) { create_or_update_project_cluster_integration_path(project, cluster) }
diff --git a/spec/requests/projects/merge_requests/creations_spec.rb b/spec/requests/projects/merge_requests/creations_spec.rb
index 0a3e663444f..842ad01656e 100644
--- a/spec/requests/projects/merge_requests/creations_spec.rb
+++ b/spec/requests/projects/merge_requests/creations_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'merge requests creations' do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
before do
login_as(user)
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index 6cf7bfb1795..c761af86c16 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'merge requests discussions' do
# Further tests can be found at merge_requests_controller_spec.rb
describe 'GET /:namespace/:project/-/merge_requests/:iid/discussions' do
let(:project) { create(:project, :repository, :public) }
- let(:owner) { project.owner }
+ let(:owner) { project.first_owner }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
diff --git a/spec/requests/projects/merge_requests_spec.rb b/spec/requests/projects/merge_requests_spec.rb
index 59fde803560..91153554e55 100644
--- a/spec/requests/projects/merge_requests_spec.rb
+++ b/spec/requests/projects/merge_requests_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'merge requests actions' do
reviewers: [user2])
end
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:user2) { create(:user) }
before do
diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb
index c248463faa3..61bfe1c6edf 100644
--- a/spec/requests/projects/metrics_dashboard_spec.rb
+++ b/spec/requests/projects/metrics_dashboard_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects::MetricsDashboardController' do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:environment2) { create(:environment, project: project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
before do
project.add_developer(user)
diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb
index 2bf1ffb2edc..44ee50ca002 100644
--- a/spec/requests/projects/noteable_notes_spec.rb
+++ b/spec/requests/projects/noteable_notes_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Project noteable notes' do
let(:etag_store) { Gitlab::EtagCaching::Store.new }
let(:notes_path) { project_noteable_notes_path(project, target_type: merge_request.class.name.underscore, target_id: merge_request.id) }
let(:project) { merge_request.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:response_etag) { response.headers['ETag'] }
let(:stored_etag) { "W/\"#{etag_store.get(notes_path)}\"" }
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index ef7741c2d0f..ed043bacf31 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -148,6 +148,32 @@ RSpec.describe Projects::AutocompleteService do
end
end
+ describe '#contacts' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:contact_1) { create(:contact, group: group) }
+ let_it_be(:contact_2) { create(:contact, group: group) }
+
+ subject { described_class.new(project, user).contacts.as_json }
+
+ before do
+ stub_feature_flags(customer_relations: true)
+ group.add_developer(user)
+ end
+
+ it 'returns contact data correctly' do
+ expected_contacts = [
+ { 'id' => contact_1.id, 'email' => contact_1.email,
+ 'first_name' => contact_1.first_name, 'last_name' => contact_1.last_name },
+ { 'id' => contact_2.id, 'email' => contact_2.email,
+ 'first_name' => contact_2.first_name, 'last_name' => contact_2.last_name }
+ ]
+
+ expect(subject).to match_array(expected_contacts)
+ end
+ end
+
describe '#labels_as_hash' do
def expect_labels_to_equal(labels, expected_labels)
expect(labels.size).to eq(expected_labels.size)