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_list_deprecated.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue3
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue3
-rw-r--r--app/assets/javascripts/ide/components/ide_sidebar_nav.vue3
-rw-r--r--app/assets/javascripts/lib/utils/constants.js3
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue3
-rw-r--r--app/models/bulk_imports/entity.rb15
-rw-r--r--changelogs/unreleased/290715-change-project-has_external_wiki-to-be-not-null.yml6
-rw-r--r--changelogs/unreleased/Replace-bootstrap-event-strings-with-constants.yml5
-rw-r--r--changelogs/unreleased/kassio-bulkimports-avoid-recursive-group-migration.yml5
-rw-r--r--changelogs/unreleased/sy-add-shift-timeframe-index.yml5
-rw-r--r--config/feature_flags/development/instance_statistics.yml2
-rw-r--r--config/feature_flags/development/optimized_issuable_label_filter.yml2
-rw-r--r--config/feature_flags/development/similarity_search.yml2
-rw-r--r--config/feature_flags/development/track_unique_visits.yml2
-rw-r--r--db/migrate/20210105025900_add_default_projects_has_external_wiki.rb19
-rw-r--r--db/migrate/20210129225244_add_index_to_oncall_shfts_on_starts_at_and_ends_at.rb24
-rw-r--r--db/post_migrate/20210105025903_add_not_null_constraint_to_projects_has_external_wiki.rb16
-rw-r--r--db/post_migrate/20210105030124_cleanup_projects_with_null_has_external_wiki.rb89
-rw-r--r--db/schema_migrations/202101050259001
-rw-r--r--db/schema_migrations/202101050259031
-rw-r--r--db/schema_migrations/202101050301241
-rw-r--r--db/schema_migrations/202101292252441
-rw-r--r--db/structure.sql9
-rw-r--r--doc/integration/saml.md16
-rw-r--r--doc/push_rules/push_rules.md10
-rw-r--r--doc/user/shortcuts.md1
-rwxr-xr-xlib/support/init.d/gitlab4
-rw-r--r--locale/gitlab.pot5
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js3
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js3
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js3
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js3
-rw-r--r--spec/migrations/cleanup_projects_with_null_has_external_wiki_spec.rb104
-rw-r--r--spec/models/bulk_imports/entity_spec.rb31
-rw-r--r--spec/models/project_spec.rb27
46 files changed, 424 insertions, 41 deletions
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 24aef5d6187..72d98149153 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -3,6 +3,7 @@ import { Sortable, MultiDrag } from 'sortablejs';
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import {
@@ -167,7 +168,7 @@ export default {
boardsStore.startMoving(list, issue);
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
sortableStart();
},
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 4de1f983db6..d2d8784ce14 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -13,6 +13,7 @@ import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { isListDraggable } from '~/boards/boards_util';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { inactiveId, LIST, ListType } from '../constants';
import eventHub from '../eventhub';
import AccessorUtilities from '../../lib/utils/accessor';
@@ -158,7 +159,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
index 7bc2c7d200d..196c58537bc 100644
--- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
@@ -17,6 +17,7 @@ import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
import { inactiveId, LIST, ListType } from '../constants';
import IssueCount from './issue_count.vue';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards
@@ -142,7 +143,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
index 500ad9c854f..a2dbd52369f 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -10,6 +10,7 @@ import {
} from '@gitlab/ui';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import createFlash from '~/flash';
+import { BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
import projectMilestones from '../../graphql/project_milestones.query.graphql';
@@ -73,7 +74,7 @@ export default {
},
},
mounted() {
- this.$root.$on('bv::dropdown::hide', () => {
+ this.$root.$on(BV_DROPDOWN_HIDE, () => {
this.$refs.sidebarItem.collapse();
});
},
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 219d742f8b3..26a175cea51 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -11,6 +11,7 @@ import {
CONFLICT_THEIR,
CONFLICT_MARKER,
} from '../constants';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
@@ -162,7 +163,7 @@ export default {
this.$emit('enterdragging', { ...line, index });
},
onDragStart(line) {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
this.dragging = true;
this.$emit('startdragging', line);
},
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
index 233f426af6c..0dea04f7c7d 100644
--- a/app/assets/javascripts/environments/components/environment_delete.vue
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -6,6 +6,7 @@
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import eventHub from '../event_hub';
export default {
@@ -40,7 +41,7 @@ export default {
},
methods: {
onClick() {
- this.$root.$emit('bv::hide::tooltip', this.$options.deleteEnvironmentTooltipId);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.deleteEnvironmentTooltipId);
eventHub.$emit('requestDeleteEnvironment', this.environment);
},
onDeleteEnvironment(environment) {
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index 8e100623199..adb0b05d002 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -6,6 +6,7 @@
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import eventHub from '../event_hub';
export default {
@@ -40,7 +41,7 @@ export default {
},
methods: {
onClick() {
- this.$root.$emit('bv::hide::tooltip', this.$options.stopEnvironmentTooltipId);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.stopEnvironmentTooltipId);
eventHub.$emit('requestStopEnvironment', this.environment);
},
onStopEnvironment(environment) {
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 16e70382fdd..edb7e373f6f 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { leftSidebarViews } from '../constants';
export default {
@@ -20,7 +21,7 @@ export default {
this.updateActivityBarView(view);
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
},
leftSidebarViews,
diff --git a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
index 9dbed0ace40..6bc84eb90c6 100644
--- a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
+++ b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { otherSide } from '../utils';
import { SIDE_RIGHT } from '../constants';
@@ -50,7 +51,7 @@ export default {
},
clickTab(e, tab) {
e.currentTarget.blur();
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
if (this.isActiveTab(tab)) {
this.$emit('close');
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 82040205be5..b19a4a01a5f 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -13,3 +13,6 @@ export const DATETIME_RANGE_TYPES = {
export const BV_SHOW_MODAL = 'bv::show::modal';
export const BV_HIDE_MODAL = 'bv::hide::modal';
+export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
+export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
+export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index d2fb1cef5b4..bc8e1d3fec6 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -6,6 +6,7 @@ import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { splitCamelCase } from '../../lib/utils/text_utility';
import ReplyButton from './note_actions/reply_button.vue';
@@ -193,7 +194,7 @@ export default {
},
closeTooltip() {
this.$nextTick(() => {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
});
},
handleAssigneeUpdate(assignees) {
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 0ce94d4f02f..949d0d30297 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -3,6 +3,7 @@ import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { reportToSentry } from './utils';
@@ -62,7 +63,7 @@ export default {
*
*/
onClickAction() {
- this.$root.$emit('bv::hide::tooltip', `js-ci-action-${this.link}`);
+ this.$root.$emit(BV_HIDE_TOOLTIP, `js-ci-action-${this.link}`);
this.isDisabled = true;
this.isLoading = true;
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index df0d632a22d..ae41ca200d0 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -4,6 +4,7 @@ import { sprintf } from '~/locale';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { accessValue } from './accessors';
import { REST } from './constants';
import { reportToSentry } from './utils';
@@ -144,7 +145,7 @@ export default {
},
methods: {
hideTooltips() {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index d18e604f087..22f9fb72159 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -2,6 +2,7 @@
import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { accessValue } from './accessors';
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
import { reportToSentry } from './utils';
@@ -126,7 +127,7 @@ export default {
this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
},
hideTooltips() {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
onDownstreamHovered() {
this.$emit('downstreamHovered', this.sourceJobName);
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index e3a7f144321..7b36d57dfbf 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -2,6 +2,7 @@
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { uniqueId } from 'lodash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
export default {
components: {
@@ -76,7 +77,7 @@ export default {
});
this.clipboard
.on('success', (e) => {
- this.$root.$emit('bv::hide::tooltip', this.id);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.id);
this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index a4d0b7485ba..16224fde502 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -43,6 +43,8 @@ class BulkImports::Entity < ApplicationRecord
validate :validate_parent_is_a_group, if: :parent
validate :validate_imported_entity_type
+ validate :validate_destination_namespace_ascendency, if: :group_entity?
+
enum source_type: { group_entity: 0, project_entity: 1 }
state_machine :status, initial: :created do
@@ -107,4 +109,17 @@ class BulkImports::Entity < ApplicationRecord
)
end
end
+
+ def validate_destination_namespace_ascendency
+ source = Group.find_by_full_path(source_full_path)
+
+ return unless source
+
+ if source.self_and_descendants.any? { |namespace| namespace.full_path == destination_namespace }
+ errors.add(
+ :destination_namespace,
+ s_('BulkImport|destination group cannot be part of the source group tree')
+ )
+ end
+ end
end
diff --git a/changelogs/unreleased/290715-change-project-has_external_wiki-to-be-not-null.yml b/changelogs/unreleased/290715-change-project-has_external_wiki-to-be-not-null.yml
new file mode 100644
index 00000000000..b2534a7fc8c
--- /dev/null
+++ b/changelogs/unreleased/290715-change-project-has_external_wiki-to-be-not-null.yml
@@ -0,0 +1,6 @@
+---
+title: Change projects.has_external_wiki column to be not null and default to false,
+ and cleanup all null and incorrect data
+merge_request: 50916
+author:
+type: changed
diff --git a/changelogs/unreleased/Replace-bootstrap-event-strings-with-constants.yml b/changelogs/unreleased/Replace-bootstrap-event-strings-with-constants.yml
new file mode 100644
index 00000000000..c0661ecd14a
--- /dev/null
+++ b/changelogs/unreleased/Replace-bootstrap-event-strings-with-constants.yml
@@ -0,0 +1,5 @@
+---
+title: Replace bootstrap event strings with constants
+merge_request: 52777
+author: Kev @KevSlashNull
+type: other
diff --git a/changelogs/unreleased/kassio-bulkimports-avoid-recursive-group-migration.yml b/changelogs/unreleased/kassio-bulkimports-avoid-recursive-group-migration.yml
new file mode 100644
index 00000000000..7025c2632bd
--- /dev/null
+++ b/changelogs/unreleased/kassio-bulkimports-avoid-recursive-group-migration.yml
@@ -0,0 +1,5 @@
+---
+title: 'BulkImports: avoid infinity recursion on group migration'
+merge_request: 52931
+author:
+type: fixed
diff --git a/changelogs/unreleased/sy-add-shift-timeframe-index.yml b/changelogs/unreleased/sy-add-shift-timeframe-index.yml
new file mode 100644
index 00000000000..2c92c54d72a
--- /dev/null
+++ b/changelogs/unreleased/sy-add-shift-timeframe-index.yml
@@ -0,0 +1,5 @@
+---
+title: Add index to incident management oncall shifts table
+merge_request: 52961
+author:
+type: performance
diff --git a/config/feature_flags/development/instance_statistics.yml b/config/feature_flags/development/instance_statistics.yml
index b5354561d03..3caddad8b69 100644
--- a/config/feature_flags/development/instance_statistics.yml
+++ b/config/feature_flags/development/instance_statistics.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40583
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241711
milestone: '13.4'
type: development
-group: group::analytics
+group: group::optimize
default_enabled: true
diff --git a/config/feature_flags/development/optimized_issuable_label_filter.yml b/config/feature_flags/development/optimized_issuable_label_filter.yml
index 343f40074f0..8385b85e143 100644
--- a/config/feature_flags/development/optimized_issuable_label_filter.yml
+++ b/config/feature_flags/development/optimized_issuable_label_filter.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34503
rollout_issue_url:
milestone: '13.4'
type: development
-group: group::analytics
+group: group::optimize
default_enabled: true
diff --git a/config/feature_flags/development/similarity_search.yml b/config/feature_flags/development/similarity_search.yml
index 312ab767eb0..978095c94b5 100644
--- a/config/feature_flags/development/similarity_search.yml
+++ b/config/feature_flags/development/similarity_search.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37300/
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38675
milestone: '13.3'
type: development
-group: group::analytics
+group: group::optimize
default_enabled: true
diff --git a/config/feature_flags/development/track_unique_visits.yml b/config/feature_flags/development/track_unique_visits.yml
index 80d969cc65b..b11539e830b 100644
--- a/config/feature_flags/development/track_unique_visits.yml
+++ b/config/feature_flags/development/track_unique_visits.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33146
rollout_issue_url:
milestone: '13.2'
type: development
-group: group::analytics
+group: group::optimize
default_enabled: true
diff --git a/db/migrate/20210105025900_add_default_projects_has_external_wiki.rb b/db/migrate/20210105025900_add_default_projects_has_external_wiki.rb
new file mode 100644
index 00000000000..cd74208d58c
--- /dev/null
+++ b/db/migrate/20210105025900_add_default_projects_has_external_wiki.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddDefaultProjectsHasExternalWiki < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ change_column_default(:projects, :has_external_wiki, from: nil, to: false)
+ end
+ end
+
+ def down
+ with_lock_retries do
+ change_column_default(:projects, :has_external_wiki, from: false, to: nil)
+ end
+ end
+end
diff --git a/db/migrate/20210129225244_add_index_to_oncall_shfts_on_starts_at_and_ends_at.rb b/db/migrate/20210129225244_add_index_to_oncall_shfts_on_starts_at_and_ends_at.rb
new file mode 100644
index 00000000000..8285aceb24a
--- /dev/null
+++ b/db/migrate/20210129225244_add_index_to_oncall_shfts_on_starts_at_and_ends_at.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddIndexToOncallShftsOnStartsAtAndEndsAt < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ NEW_NAME = 'index_oncall_shifts_on_rotation_id_and_starts_at_and_ends_at'
+ OLD_NAME = 'index_incident_management_oncall_shifts_on_rotation_id'
+
+ def up
+ add_concurrent_index :incident_management_oncall_shifts, %i[rotation_id starts_at ends_at], name: NEW_NAME
+
+ remove_concurrent_index_by_name :incident_management_oncall_shifts, OLD_NAME
+ end
+
+ def down
+ add_concurrent_index :incident_management_oncall_shifts, :rotation_id, name: OLD_NAME
+
+ remove_concurrent_index_by_name :incident_management_oncall_shifts, NEW_NAME
+ end
+end
diff --git a/db/post_migrate/20210105025903_add_not_null_constraint_to_projects_has_external_wiki.rb b/db/post_migrate/20210105025903_add_not_null_constraint_to_projects_has_external_wiki.rb
new file mode 100644
index 00000000000..0560258592f
--- /dev/null
+++ b/db/post_migrate/20210105025903_add_not_null_constraint_to_projects_has_external_wiki.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddNotNullConstraintToProjectsHasExternalWiki < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_not_null_constraint :projects, :has_external_wiki, validate: false
+ end
+
+ def down
+ remove_not_null_constraint :projects, :has_external_wiki
+ end
+end
diff --git a/db/post_migrate/20210105030124_cleanup_projects_with_null_has_external_wiki.rb b/db/post_migrate/20210105030124_cleanup_projects_with_null_has_external_wiki.rb
new file mode 100644
index 00000000000..7995ac6a393
--- /dev/null
+++ b/db/post_migrate/20210105030124_cleanup_projects_with_null_has_external_wiki.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+class CleanupProjectsWithNullHasExternalWiki < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ TMP_INDEX_NAME = 'tmp_index_projects_on_id_where_has_external_wiki_is_true_null'.freeze
+ BATCH_SIZE = 100
+
+ disable_ddl_transaction!
+
+ class Service < ActiveRecord::Base
+ include EachBatch
+ belongs_to :project
+
+ self.table_name = 'services'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+ end
+
+ def up
+ update_projects_with_active_external_wikis
+ update_projects_without_active_external_wikis
+ end
+
+ def down
+ # no-op : can't go back to `NULL` without first dropping the `NOT NULL` constraint
+ end
+
+ private
+
+ def update_projects_with_active_external_wikis
+ # 11 projects are scoped in this query on GitLab.com.
+ scope = Service.where(active: true, type: 'ExternalWikiService').where.not(project_id: nil)
+
+ scope.each_batch(of: BATCH_SIZE) do |relation|
+ scope_with_projects = relation
+ .joins(:project)
+ .select('project_id')
+ .merge(Project.where(has_external_wiki: [false, nil]).where(pending_delete: false).where(archived: false))
+
+ execute(<<~SQL)
+ WITH project_ids_to_update (id) AS (
+ #{scope_with_projects.to_sql}
+ )
+ UPDATE projects SET has_external_wiki = true WHERE id IN (SELECT id FROM project_ids_to_update)
+ SQL
+ end
+ end
+
+ def update_projects_without_active_external_wikis
+ # Add a temporary index to speed up the scoping of projects.
+ index_where = <<~SQL
+ (
+ "projects"."has_external_wiki" = TRUE
+ OR "projects"."has_external_wiki" IS NULL
+ )
+ AND "projects"."pending_delete" = FALSE
+ AND "projects"."archived" = FALSE
+ SQL
+
+ add_concurrent_index(:projects, :id, where: index_where, name: TMP_INDEX_NAME)
+
+ services_sub_query = Service
+ .select('1')
+ .where('services.project_id = projects.id')
+ .where(type: 'ExternalWikiService')
+ .where(active: true)
+
+ # 322 projects are scoped in this query on GitLab.com.
+ Project.where(index_where).each_batch(of: BATCH_SIZE) do |relation|
+ relation_with_exists_query = relation.where('NOT EXISTS (?)', services_sub_query)
+ execute(<<~SQL)
+ WITH project_ids_to_update (id) AS (
+ #{relation_with_exists_query.select(:id).to_sql}
+ )
+ UPDATE projects SET has_external_wiki = false WHERE id IN (SELECT id FROM project_ids_to_update)
+ SQL
+ end
+
+ # Drop the temporary index.
+ remove_concurrent_index_by_name(:projects, TMP_INDEX_NAME)
+ end
+end
diff --git a/db/schema_migrations/20210105025900 b/db/schema_migrations/20210105025900
new file mode 100644
index 00000000000..200ee5b7ff2
--- /dev/null
+++ b/db/schema_migrations/20210105025900
@@ -0,0 +1 @@
+047dd77352eda8134e55047e2d3fab07bbcd6eb41600cefb9b581d32e441fb68 \ No newline at end of file
diff --git a/db/schema_migrations/20210105025903 b/db/schema_migrations/20210105025903
new file mode 100644
index 00000000000..6680ba6bbac
--- /dev/null
+++ b/db/schema_migrations/20210105025903
@@ -0,0 +1 @@
+339d828635107f77116ffd856abcb8f8f64985cabb82c0c34ab76056f5756d2e \ No newline at end of file
diff --git a/db/schema_migrations/20210105030124 b/db/schema_migrations/20210105030124
new file mode 100644
index 00000000000..6fa3d1b1474
--- /dev/null
+++ b/db/schema_migrations/20210105030124
@@ -0,0 +1 @@
+30f3cbc0f96848f72a188616503eb80d38c33769c8bebf86a5922b374750d066 \ No newline at end of file
diff --git a/db/schema_migrations/20210129225244 b/db/schema_migrations/20210129225244
new file mode 100644
index 00000000000..1b05096b07f
--- /dev/null
+++ b/db/schema_migrations/20210129225244
@@ -0,0 +1 @@
+6cb54c71a9835ec1b3cf801a19c2cd385d224e0438c7924b6a29d298ecebe8a7 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index ce7b51d067c..ddf6105bcd6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16129,7 +16129,7 @@ CREATE TABLE projects (
repository_storage character varying DEFAULT 'default'::character varying NOT NULL,
repository_read_only boolean,
request_access_enabled boolean DEFAULT true NOT NULL,
- has_external_wiki boolean,
+ has_external_wiki boolean DEFAULT false,
ci_config_path character varying,
lfs_enabled boolean,
description_html text,
@@ -19662,6 +19662,9 @@ ALTER TABLE ONLY chat_teams
ALTER TABLE vulnerability_scanners
ADD CONSTRAINT check_37608c9db5 CHECK ((char_length(vendor) <= 255)) NOT VALID;
+ALTER TABLE projects
+ ADD CONSTRAINT check_421d399b70 CHECK ((has_external_wiki IS NOT NULL)) NOT VALID;
+
ALTER TABLE group_import_states
ADD CONSTRAINT check_cda75c7c3f CHECK ((user_id IS NOT NULL)) NOT VALID;
@@ -22127,8 +22130,6 @@ CREATE INDEX index_incident_management_oncall_schedules_on_project_id ON inciden
CREATE INDEX index_incident_management_oncall_shifts_on_participant_id ON incident_management_oncall_shifts USING btree (participant_id);
-CREATE INDEX index_incident_management_oncall_shifts_on_rotation_id ON incident_management_oncall_shifts USING btree (rotation_id);
-
CREATE UNIQUE INDEX index_index_statuses_on_project_id ON index_statuses USING btree (project_id);
CREATE INDEX index_insights_on_namespace_id ON insights USING btree (namespace_id);
@@ -22575,6 +22576,8 @@ CREATE INDEX index_onboarding_progresses_for_verify_track ON onboarding_progress
CREATE UNIQUE INDEX index_onboarding_progresses_on_namespace_id ON onboarding_progresses USING btree (namespace_id);
+CREATE INDEX index_oncall_shifts_on_rotation_id_and_starts_at_and_ends_at ON incident_management_oncall_shifts USING btree (rotation_id, starts_at, ends_at);
+
CREATE INDEX index_open_project_tracker_data_on_service_id ON open_project_tracker_data USING btree (service_id);
CREATE INDEX index_operations_feature_flags_issues_on_issue_id ON operations_feature_flags_issues USING btree (issue_id);
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 2164bc3dc5f..9b6ad3f2755 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -168,6 +168,16 @@ These groups are checked on each SAML login and user attributes updated as neces
This feature **does not** allow you to
automatically add users to GitLab [Groups](../user/group/index.md).
+Support for these groups depends on your [subscription](https://about.gitlab.com/pricing/)
+and whether you've installed [GitLab Enterprise Edition (EE)](https://about.gitlab.com/install/).
+
+| Group | Tier | GitLab Enterprise Edition (EE) Only? |
+|------------------------------|--------------------|--------------------------------------|
+| [Required](#required-groups) | **(FREE SELF)** | Yes |
+| [External](#external-groups) | **(FREE SELF)** | No |
+| [Admin](#admin-groups) | **(FREE SELF)** | Yes |
+| [Auditor](#auditor-groups) | **(PREMIUM SELF)** | Yes |
+
### Requirements
First you need to tell GitLab where to look for group information. For this you
@@ -189,7 +199,7 @@ The name of the attribute can be anything you like, but it must contain the grou
to which a user belongs. In order to tell GitLab where to find these groups, you need
to add a `groups_attribute:` element to your SAML settings.
-### Required groups **(PREMIUM SELF)**
+### Required groups **(FREE SELF)**
Your IdP passes Group Information to the SP (GitLab) in the SAML Response. You need to configure GitLab to identify:
@@ -215,7 +225,7 @@ Example:
} }
```
-### External groups **(PREMIUM SELF)**
+### External groups **(FREE SELF)**
SAML login supports automatic identification on whether a user should be considered an [external user](../user/permissions.md#external-users). This is based on the user's group membership in the SAML identity provider.
@@ -233,7 +243,7 @@ SAML login supports automatic identification on whether a user should be conside
} }
```
-### Admin groups **(PREMIUM SELF)**
+### Admin groups **(FREE SELF)**
The requirements are the same as the previous settings, your IdP needs to pass Group information to GitLab, you need to tell
GitLab where to look for the groups in the SAML response, and which group(s) should be
diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md
index 37880f22ffe..c3816155121 100644
--- a/doc/push_rules/push_rules.md
+++ b/doc/push_rules/push_rules.md
@@ -89,12 +89,12 @@ The following options are available:
| Push rule | Description |
|---------------------------------|-------------|
| Removal of tags with `git push` | Forbid users to remove Git tags with `git push`. Tags can be deleted through the web UI. |
-| Check whether author is a GitLab user | Restrict commits by author (email) to existing GitLab users. |
-| Committer restriction **(PREMIUM)** | GitLab rejects any commit that was not committed by the current authenticated user. |
+| Check whether the commit author is a GitLab user | Restrict commits to existing GitLab users (checked against their emails). |
+| Reject unverified users **(PREMIUM)** | GitLab rejects any commit that was not committed by an authenticated user. |
| Check whether commit is signed through GPG **(PREMIUM)** | Reject commit when it is not signed through GPG. Read [signing commits with GPG](../user/project/repository/gpg_signed_commits/index.md). |
-| Prevent committing secrets to Git | GitLab rejects any files that are likely to contain secrets. Read [what files are forbidden](#prevent-pushing-secrets-to-the-repository). |
-| Restrict by commit message | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
-| Restrict by commit message (negative match) | Only commit messages that do not match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
+| Prevent pushing secret files | GitLab rejects any files that are likely to contain secrets. See the [forbiden file names](#prevent-pushing-secrets-to-the-repository). |
+| Require expression in commit messages | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
+| Reject expression in commit messages | Only commit messages that do not match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
| Restrict by branch name | Only branch names that match this regular expression are allowed to be pushed. Leave empty to allow any branch name. |
| Restrict by commit author's email | Only commit author's email that match this regular expression are allowed to be pushed. Leave empty to allow any email. |
| Prohibited file names | Any committed filenames that match this regular expression and do not already exist in the repository are not allowed to be pushed. Leave empty to allow any filenames. See [common examples](#prohibited-file-names). |
diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md
index 73823dd15e8..a8435424d62 100644
--- a/doc/user/shortcuts.md
+++ b/doc/user/shortcuts.md
@@ -35,6 +35,7 @@ These shortcuts are available in most areas of GitLab
| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your Merge requests page.|
| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar. |
+| <kbd>g</kbd> + <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/). |
Additionally, the following shortcuts are available when editing text in text fields,
for example comments, replies, issue descriptions, and merge request descriptions:
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 98cac0b0d1d..4fe7f0cf480 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -138,8 +138,8 @@ wait_for_pids(){
i=$((i+1))
if [ $((i%10)) = 0 ]; then
echo -n "."
- elif [ $((i)) = 301 ]; then
- echo "Waited 30s for the processes to write their pids, something probably went wrong."
+ elif [ $((i)) = 601 ]; then
+ echo "Waited 60s for the processes to write their pids, something probably went wrong."
exit 1;
fi
done
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9349b88b3df..8c0ae91fc30 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4926,6 +4926,9 @@ msgstr ""
msgid "BulkImport|Update of import statuses with realtime changes failed"
msgstr ""
+msgid "BulkImport|destination group cannot be part of the source group tree"
+msgstr ""
+
msgid "BulkImport|expected an associated Group but has an associated Project"
msgstr ""
@@ -23774,7 +23777,7 @@ msgstr ""
msgid "Push to create a project"
msgstr ""
-msgid "PushRule|Committer restriction"
+msgid "PushRule|Reject unverified users"
msgstr ""
msgid "Pushed"
diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
index 6b4cb9bd03d..841e19532a1 100644
--- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js
+++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
@@ -3,6 +3,7 @@ import { GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import { SIDE_RIGHT, SIDE_LEFT } from '~/ide/constants';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
const TEST_TABS = [
{
@@ -74,7 +75,7 @@ describe('ide/components/ide_sidebar_nav', () => {
createComponent({ isOpen, side });
bsTooltipHide = jest.fn();
- wrapper.vm.$root.$on('bv::hide::tooltip', bsTooltipHide);
+ wrapper.vm.$root.$on(BV_HIDE_TOOLTIP, bsTooltipHide);
});
it('renders buttons', () => {
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 96a388614f3..3cc52379026 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -6,6 +6,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import waitForPromises from 'helpers/wait_for_promises';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
import { member } from '../../mock_data';
const localVue = createLocalVue();
@@ -67,7 +68,7 @@ describe('RoleDropdown', () => {
createComponent();
findDropdownToggle().trigger('click');
- wrapper.vm.$root.$on('bv::dropdown::shown', () => {
+ wrapper.vm.$root.$on(BV_DROPDOWN_SHOW, () => {
done();
});
});
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index dfd7a2aae28..4661e4b1279 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -6,6 +6,7 @@ import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import axios from '~/lib/utils/axios_utils';
import { userDataMock } from '../mock_data';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
describe('noteActions', () => {
let wrapper;
@@ -135,7 +136,7 @@ describe('noteActions', () => {
.then(() => {
const emitted = Object.keys(rootWrapper.emitted());
- expect(emitted).toEqual(['bv::hide::tooltip']);
+ expect(emitted).toEqual([BV_HIDE_TOOLTIP]);
done();
})
.catch(done.fail);
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index dad83c4d86e..75537104773 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -4,6 +4,7 @@ import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipelin
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import mockData from './linked_pipelines_mock_data';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
const mockPipeline = mockData.triggered[0];
const validTriggeredPipelineId = mockPipeline.project.id;
@@ -212,11 +213,11 @@ describe('Linked pipeline', () => {
expect(wrapper.emitted().pipelineClicked).toBeTruthy();
});
- it('should emit `bv::hide::tooltip` to close the tooltip', () => {
+ it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
jest.spyOn(wrapper.vm.$root, '$emit');
findButton().trigger('click');
- expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual(['bv::hide::tooltip']);
+ expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([BV_HIDE_TOOLTIP]);
});
it('should emit downstreamHovered with job name on mouseover', () => {
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index ca9f8ff54d4..97d4313b89e 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -1,5 +1,6 @@
import { shallowMount, createWrapper } from '@vue/test-utils';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
describe('modal copy button', () => {
let wrapper;
@@ -31,7 +32,7 @@ describe('modal copy button', () => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().success).not.toBeEmpty();
expect(document.execCommand).toHaveBeenCalledWith('copy');
- expect(root.emitted('bv::hide::tooltip')).toEqual([['test-id']]);
+ expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]);
});
});
it("should propagate the clipboard error event if execCommand doesn't work", () => {
diff --git a/spec/migrations/cleanup_projects_with_null_has_external_wiki_spec.rb b/spec/migrations/cleanup_projects_with_null_has_external_wiki_spec.rb
new file mode 100644
index 00000000000..c3be936f0f1
--- /dev/null
+++ b/spec/migrations/cleanup_projects_with_null_has_external_wiki_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe CleanupProjectsWithNullHasExternalWiki, :migration, schema: 20210105025900 do
+ let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
+ let(:projects) { table(:projects) }
+ let(:services) { table(:services) }
+ let(:constraint_name) { 'check_421d399b70' }
+
+ def create_projects!(num)
+ Array.new(num) do
+ projects.create!(namespace_id: namespace.id)
+ end
+ end
+
+ def create_active_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ def create_disabled_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: false)
+ end
+ end
+
+ def create_active_other_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'NotAnExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ it 'sets `projects.has_external_wiki` correctly' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+
+ project_with_external_wiki_1,
+ project_with_external_wiki_2,
+ project_with_external_wiki_3,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2,
+ project_with_disabled_external_wiki_3,
+ project_without_external_wiki_1,
+ project_without_external_wiki_2,
+ project_without_external_wiki_3 = create_projects!(9)
+
+ create_active_external_wiki_integrations!(
+ project_with_external_wiki_1,
+ project_with_external_wiki_2,
+ project_with_external_wiki_3
+ )
+
+ create_disabled_external_wiki_integrations!(
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2,
+ project_with_disabled_external_wiki_3
+ )
+
+ create_active_other_integrations!(
+ project_without_external_wiki_1,
+ project_without_external_wiki_2,
+ project_without_external_wiki_3
+ )
+
+ # PG triggers on the services table added in a previous migration
+ # will have set the `has_external_wiki` columns to correct data when
+ # the services records were created above.
+ #
+ # We set the `has_external_wiki` columns for projects to NULL or incorrect
+ # data manually below to emulate projects in a state before the PG
+ # triggers were added.
+ project_with_external_wiki_1.update!(has_external_wiki: nil)
+ project_with_external_wiki_2.update!(has_external_wiki: false)
+
+ project_with_disabled_external_wiki_1.update!(has_external_wiki: nil)
+ project_with_disabled_external_wiki_2.update!(has_external_wiki: true)
+
+ project_without_external_wiki_1.update!(has_external_wiki: nil)
+ project_without_external_wiki_2.update!(has_external_wiki: true)
+
+ migrate!
+
+ expected_true = [
+ project_with_external_wiki_1,
+ project_with_external_wiki_2,
+ project_with_external_wiki_3
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expected_false = [
+ project_without_external_wiki_1,
+ project_without_external_wiki_2,
+ project_without_external_wiki_3,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2,
+ project_with_disabled_external_wiki_3
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expect(expected_true).to all(eq(true))
+ expect(expected_false).to all(eq(false))
+ end
+end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 0732b671729..e5ab96ca514 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -81,6 +81,37 @@ RSpec.describe BulkImports::Entity, type: :model do
expect(entity.errors).to include(:parent)
end
end
+
+ context 'validate destination namespace of a group_entity' do
+ it 'is invalid if destination namespace is the source namespace' do
+ group_a = create(:group, path: 'group_a')
+
+ entity = build(
+ :bulk_import_entity,
+ :group_entity,
+ source_full_path: group_a.full_path,
+ destination_namespace: group_a.full_path
+ )
+
+ expect(entity).not_to be_valid
+ expect(entity.errors).to include(:destination_namespace)
+ end
+
+ it 'is invalid if destination namespace is a descendant of the source' do
+ group_a = create(:group, path: 'group_a')
+ group_b = create(:group, parent: group_a, path: 'group_b')
+
+ entity = build(
+ :bulk_import_entity,
+ :group_entity,
+ source_full_path: group_a.full_path,
+ destination_namespace: group_b.full_path
+ )
+
+ expect(entity).not_to be_valid
+ expect(entity.errors).to include(:destination_namespace)
+ end
+ end
end
describe "#update_tracker_for" do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 59464c89dc1..7a7b7af8afe 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1145,11 +1145,32 @@ RSpec.describe Project, factory_default: :keep do
is_expected.to eq(nil)
end
- it 'sets Project#has_external_wiki when it is nil' do
+ it 'calls Project#cache_has_external_wiki when `has_external_wiki` is nil' do
+ project = build(:project, has_external_wiki: nil)
+
+ expect(project).to receive(:cache_has_external_wiki)
+
+ project.external_wiki
+ end
+
+ it 'does not call Project#cache_has_external_wiki when `has_external_wiki` is not nil' do
+ project = build(:project)
+
+ expect(project).not_to receive(:cache_has_external_wiki)
+
+ project.external_wiki
+ end
+ end
+
+ describe '#cache_has_external_wiki (private method)' do
+ it 'sets Project#has_external_wiki correctly, affecting Project#external_wiki' do
+ project = create(:project)
create(:service, project: project, type: 'ExternalWikiService', active: true)
- project.update_column(:has_external_wiki, nil)
+ project.update_column(:has_external_wiki, false)
- expect { subject }.to change { project.has_external_wiki }.from(nil).to(true)
+ expect { project.send(:cache_has_external_wiki) }
+ .to change { project.has_external_wiki }.from(false).to(true)
+ .and(change { project.external_wiki }.from(nil).to(kind_of(ExternalWikiService)))
end
end