summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md10
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue173
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue106
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss24
-rw-r--r--app/controllers/admin/groups_controller.rb1
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb28
-rw-r--r--app/services/projects/hashed_storage/rollback_repository_service.rb28
-rw-r--r--db/post_migrate/20201005094331_migrate_compliance_framework_enum_to_database_framework_record.rb2
-rw-r--r--db/post_migrate/20201104124300_ensure_namespace_settings_creation.rb40
-rw-r--r--db/schema_migrations/202011041243001
-rw-r--r--doc/user/admin_area/merge_requests_approvals.md42
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb14
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js98
-rw-r--r--spec/migrations/ensure_namespace_settings_creation_spec.rb44
-rw-r--r--spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb42
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb36
-rw-r--r--spec/services/projects/hashed_storage/rollback_repository_service_spec.rb36
22 files changed, 493 insertions, 322 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 807db37bd47..4b6c4582b5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 13.5.4 (2020-11-13)
+
+### Fixed (4 changes)
+
+- Fix Vue Labels Select dropdown keyboard scroll. !43874
+- Hashed Storage: make migration and rollback resilient to exceptions. !46178
+- Fix compliance framework database migration on CE instances. !46761
+- Resolve problem when namespace_settings were not created for groups created via admin panel. !46875
+
+
## 13.5.3 (2020-11-03)
### Fixed (3 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index cd2c0e088c7..3330217f208 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-13.5.3 \ No newline at end of file
+13.5.4 \ No newline at end of file
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
index 746e38e98e8..00c54313292 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
@@ -3,5 +3,3 @@ export const DropdownVariant = {
Standalone: 'standalone',
Embedded: 'embedded',
};
-
-export const LIST_BUFFER_SIZE = 5;
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index c8dee81d746..353dee862d0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -1,23 +1,25 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import {
+ GlIntersectionObserver,
+ GlLoadingIcon,
+ GlButton,
+ GlSearchBoxByType,
+ GlLink,
+} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import LabelItem from './label_item.vue';
-import { LIST_BUFFER_SIZE } from './constants';
-
export default {
- LIST_BUFFER_SIZE,
components: {
+ GlIntersectionObserver,
GlLoadingIcon,
GlButton,
GlSearchBoxByType,
GlLink,
- SmartVirtualList,
LabelItem,
},
data() {
@@ -46,15 +48,8 @@ export default {
}
return this.labels;
},
- showListContainer() {
- if (this.isDropdownVariantSidebar) {
- return !this.labelsFetchInProgress;
- }
-
- return true;
- },
showNoMatchingResultsMessage() {
- return !this.labelsFetchInProgress && !this.visibleLabels.length;
+ return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
},
watch: {
@@ -67,14 +62,12 @@ export default {
}
},
},
- mounted() {
- this.fetchLabels();
- },
methods: {
...mapActions([
'toggleDropdownContents',
'toggleDropdownContentsCreateView',
'fetchLabels',
+ 'receiveLabelsSuccess',
'updateSelectedLabels',
'toggleDropdownContents',
]),
@@ -100,6 +93,17 @@ export default {
}
},
/**
+ * We want to remove loaded labels to ensure component
+ * fetches fresh set of labels every time when shown.
+ */
+ handleComponentDisappear() {
+ this.receiveLabelsSuccess([]);
+ },
+ handleCreateLabelClick() {
+ this.receiveLabelsSuccess([]);
+ this.toggleDropdownContentsCreateView();
+ },
+ /**
* This method enables keyboard navigation support for
* the dropdown.
*/
@@ -135,84 +139,75 @@ export default {
</script>
<template>
- <div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
- <gl-loading-icon
- v-if="labelsFetchInProgress"
- class="labels-fetch-loading position-absolute gl-display-flex gl-align-items-center w-100 h-100"
- size="md"
- />
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
- data-testid="dropdown-title"
- >
- <span class="flex-grow-1">{{ labelsListTitle }}</span>
- <gl-button
- :aria-label="__('Close')"
- variant="link"
- size="small"
- class="dropdown-header-button gl-p-0!"
- icon="close"
- @click="toggleDropdownContents"
- />
- </div>
- <div class="dropdown-input" @click.stop="() => {}">
- <gl-search-box-by-type
- v-model="searchKey"
- :autofocus="true"
- data-qa-selector="dropdown_input_field"
- />
- </div>
- <div
- v-show="showListContainer"
- ref="labelsListContainer"
- class="dropdown-content"
- data-testid="dropdown-content"
- >
- <smart-virtual-list
- :length="visibleLabels.length"
- :remain="$options.LIST_BUFFER_SIZE"
- :size="$options.LIST_BUFFER_SIZE"
- wclass="list-unstyled mb-0"
- wtag="ul"
- class="h-100"
+ <gl-intersection-observer @appear="fetchLabels" @disappear="handleComponentDisappear">
+ <div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
+ <div
+ v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ data-testid="dropdown-title"
>
- <li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
+ <span class="flex-grow-1">{{ labelsListTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ @click="toggleDropdownContents"
+ />
+ </div>
+ <div class="dropdown-input" @click.stop="() => {}">
+ <gl-search-box-by-type
+ v-model="searchKey"
+ :autofocus="true"
+ :disabled="labelsFetchInProgress"
+ data-qa-selector="dropdown_input_field"
+ />
+ </div>
+ <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
+ <gl-loading-icon
+ v-if="labelsFetchInProgress"
+ class="labels-fetch-loading gl-align-items-center w-100 h-100"
+ size="md"
+ />
+ <ul v-else class="list-unstyled mb-0">
<label-item
+ v-for="(label, index) in visibleLabels"
+ :key="label.id"
:label="label"
:is-label-set="label.set"
:highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)"
/>
- </li>
- <li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
- {{ __('No matching results') }}
- </li>
- </smart-virtual-list>
- </div>
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-footer"
- data-testid="dropdown-footer"
- >
- <ul class="list-unstyled">
- <li v-if="allowLabelCreate">
- <gl-link
- class="gl-display-flex w-100 flex-row text-break-word label-item"
- @click="toggleDropdownContentsCreateView"
- >
- {{ footerCreateLabelTitle }}
- </gl-link>
- </li>
- <li>
- <gl-link
- :href="labelsManagePath"
- class="gl-display-flex flex-row text-break-word label-item"
- >
- {{ footerManageLabelTitle }}
- </gl-link>
- </li>
- </ul>
+ <li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
+ {{ __('No matching results') }}
+ </li>
+ </ul>
+ </div>
+ <div
+ v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
+ class="dropdown-footer"
+ data-testid="dropdown-footer"
+ >
+ <ul class="list-unstyled">
+ <li v-if="allowLabelCreate">
+ <gl-link
+ class="gl-display-flex w-100 flex-row text-break-word label-item"
+ @click="handleCreateLabelClick"
+ >
+ {{ footerCreateLabelTitle }}
+ </gl-link>
+ </li>
+ <li>
+ <gl-link
+ :href="labelsManagePath"
+ class="gl-display-flex flex-row text-break-word label-item"
+ >
+ {{ footerManageLabelTitle }}
+ </gl-link>
+ </li>
+ </ul>
+ </div>
</div>
- </div>
+ </gl-intersection-observer>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
index 002e741ab96..e431fd000a6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
@@ -1,11 +1,8 @@
<script>
-import { GlIcon, GlLink } from '@gitlab/ui';
+import { GlLink, GlIcon } from '@gitlab/ui';
export default {
- components: {
- GlIcon,
- GlLink,
- },
+ functional: true,
props: {
label: {
type: Object,
@@ -21,46 +18,65 @@ export default {
default: false,
},
},
- data() {
- return {
- isSet: this.isLabelSet,
- };
- },
- computed: {
- labelBoxStyle() {
- return {
- backgroundColor: this.label.color,
- };
- },
- },
- watch: {
- /**
- * This watcher assures that if user used
- * `Enter` key to set/unset label, changes
- * are reflected here too.
- */
- isLabelSet(value) {
- this.isSet = value;
- },
- },
- methods: {
- handleClick() {
- this.isSet = !this.isSet;
- this.$emit('clickLabel', this.label);
- },
+ render(h, { props, listeners }) {
+ const { label, highlight, isLabelSet } = props;
+
+ const labelColorBox = h('span', {
+ class: 'dropdown-label-box',
+ style: {
+ backgroundColor: label.color,
+ },
+ attrs: {
+ 'data-testid': 'label-color-box',
+ },
+ });
+
+ const checkedIcon = h(GlIcon, {
+ class: {
+ 'mr-2 align-self-center': true,
+ hidden: !isLabelSet,
+ },
+ props: {
+ name: 'mobile-issue-close',
+ },
+ });
+
+ const noIcon = h('span', {
+ class: {
+ 'mr-3 pr-2': true,
+ hidden: isLabelSet,
+ },
+ attrs: {
+ 'data-testid': 'no-icon',
+ },
+ });
+
+ const labelTitle = h('span', label.title);
+
+ const labelLink = h(
+ GlLink,
+ {
+ class: 'd-flex align-items-baseline text-break-word label-item',
+ on: {
+ click: () => {
+ listeners.clickLabel(label);
+ },
+ },
+ },
+ [noIcon, checkedIcon, labelColorBox, labelTitle],
+ );
+
+ return h(
+ 'li',
+ {
+ class: {
+ 'd-block': true,
+ 'text-left': true,
+ 'is-focused': highlight,
+ },
+ },
+ [labelLink],
+ );
},
};
</script>
-
-<template>
- <gl-link
- class="d-flex align-items-baseline text-break-word label-item"
- :class="{ 'is-focused': highlight }"
- @click="handleClick"
- >
- <gl-icon v-show="isSet" name="mobile-issue-close" class="mr-2 align-self-center" />
- <span v-show="!isSet" data-testid="no-icon" class="mr-3 pr-2"></span>
- <span class="dropdown-label-box" data-testid="label-color-box" :style="labelBoxStyle"></span>
- <span>{{ label.title }}</span>
- </gl-link>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index c651013c5f5..2f71907f772 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -266,7 +266,7 @@ export default {
</dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
- v-if="dropdownButtonVisible && showDropdownContents"
+ v-show="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
/>
</template>
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index e8d37fcf40b..ca20b18f851 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1016,6 +1016,23 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
}
}
+ li {
+ &:hover,
+ &.is-focused {
+ .label-item {
+ @include dropdown-item-hover;
+
+ text-decoration: none;
+ }
+ }
+ }
+
+ .labels-select-dropdown-button {
+ .gl-button-text {
+ width: 100%;
+ }
+ }
+
.labels-select-dropdown-contents {
min-height: $dropdown-min-height;
max-height: 330px;
@@ -1049,13 +1066,6 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.label-item {
padding: 8px 20px;
-
- &:hover,
- &.is-focused {
- @include dropdown-item-hover;
-
- text-decoration: none;
- }
}
.color-input-container {
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 032e449f995..0a1c85eef3f 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -41,6 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController
if @group.save
@group.add_owner(current_user)
+ @group.create_namespace_settings
redirect_to [:admin, @group], notice: _('Group %{group_name} was successfully created.') % { group_name: @group.name }
else
render "new"
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index fd62ac37d27..adc7e38e4d5 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -21,14 +21,32 @@ module Projects
project.storage_version = nil
end
- project.repository_read_only = false
- project.save!(validate: false)
-
- if result && block_given?
- yield
+ project.transaction do
+ project.save!(validate: false)
+ project.set_repository_writable!
end
result
+ rescue Gitlab::Git::CommandError => e
+ logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
+
+ rollback_migration!
+
+ false
+ rescue OpenSSL::Cipher::CipherError => e
+ logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
+
+ rollback_migration!
+
+ false
+ end
+
+ private
+
+ def rollback_migration!
+ rollback_folder_move
+ project.storage_version = nil
+ project.set_repository_writable!
end
end
end
diff --git a/app/services/projects/hashed_storage/rollback_repository_service.rb b/app/services/projects/hashed_storage/rollback_repository_service.rb
index d6646e3765e..6ab49630603 100644
--- a/app/services/projects/hashed_storage/rollback_repository_service.rb
+++ b/app/services/projects/hashed_storage/rollback_repository_service.rb
@@ -21,14 +21,32 @@ module Projects
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
end
- project.repository_read_only = false
- project.save!(validate: false)
-
- if result && block_given?
- yield
+ project.transaction do
+ project.save!(validate: false)
+ project.set_repository_writable!
end
result
+ rescue Gitlab::Git::CommandError => e
+ logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
+
+ rollback_migration!
+
+ false
+ rescue OpenSSL::Cipher::CipherError => e
+ logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
+
+ rollback_migration!
+
+ false
+ end
+
+ private
+
+ def rollback_migration!
+ rollback_folder_move
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
+ project.set_repository_writable!
end
end
end
diff --git a/db/post_migrate/20201005094331_migrate_compliance_framework_enum_to_database_framework_record.rb b/db/post_migrate/20201005094331_migrate_compliance_framework_enum_to_database_framework_record.rb
index 5e261637d46..a5fab8576e4 100644
--- a/db/post_migrate/20201005094331_migrate_compliance_framework_enum_to_database_framework_record.rb
+++ b/db/post_migrate/20201005094331_migrate_compliance_framework_enum_to_database_framework_record.rb
@@ -52,8 +52,6 @@ class MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord < ActiveRecord::Mi
end
def up
- return unless Gitlab.ee?
-
TmpComplianceFramework.reset_column_information
TmpProjectSettings.reset_column_information
diff --git a/db/post_migrate/20201104124300_ensure_namespace_settings_creation.rb b/db/post_migrate/20201104124300_ensure_namespace_settings_creation.rb
new file mode 100644
index 00000000000..08f92d21f44
--- /dev/null
+++ b/db/post_migrate/20201104124300_ensure_namespace_settings_creation.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class EnsureNamespaceSettingsCreation < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10000
+ MIGRATION = 'BackfillNamespaceSettings'
+ DELAY_INTERVAL = 2.minutes.to_i
+
+ disable_ddl_transaction!
+
+ class Namespace < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'namespaces'
+ end
+
+ def up
+ ensure_data_migration
+ end
+
+ def down
+ # no-op
+ end
+
+ private
+
+ def ensure_data_migration
+ Namespace.each_batch(of: BATCH_SIZE) do |query, index|
+ missing_count = query.where("NOT EXISTS (SELECT 1 FROM namespace_settings WHERE namespace_settings.namespace_id=namespaces.id)").limit(1).size
+
+ if missing_count > 0
+ ids_range = query.pluck("MIN(id), MAX(id)").flatten
+
+ migrate_in(index * DELAY_INTERVAL, MIGRATION, ids_range)
+ end
+ end
+ end
+end
diff --git a/db/schema_migrations/20201104124300 b/db/schema_migrations/20201104124300
new file mode 100644
index 00000000000..6c54da0da54
--- /dev/null
+++ b/db/schema_migrations/20201104124300
@@ -0,0 +1 @@
+e17da7eebb6d054a711368369d2b4fa684e96344f845bb7c6b3c89a9b4c4e067 \ No newline at end of file
diff --git a/doc/user/admin_area/merge_requests_approvals.md b/doc/user/admin_area/merge_requests_approvals.md
index 8f51c03e105..fb9ca21a214 100644
--- a/doc/user/admin_area/merge_requests_approvals.md
+++ b/doc/user/admin_area/merge_requests_approvals.md
@@ -5,51 +5,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference, concepts
---
-# Instance-level merge request approval rules **(PREMIUM ONLY)**
+# Merge request approval rules **(PREMIUM ONLY)**
> Introduced in [GitLab Premium](https://gitlab.com/gitlab-org/gitlab/-/issues/39060) 12.8.
-Merge request approvals rules prevent users overriding certain settings on a project
-level. When configured, only administrators can change these settings on a project level
-if they are enabled at an instance level.
+Merge request approval rules prevent users from overriding certain settings on the project
+level. When enabled at the instance level, these settings are no longer editable on the
+project level.
To enable merge request approval rules for an instance:
1. Navigate to **Admin Area >** **{push-rules}** **Push Rules** and expand **Merge
- requests approvals**.
+requests approvals**.
1. Set the required rule.
1. Click **Save changes**.
-GitLab administrators can later override these settings in a project’s settings.
-
## Available rules
Merge request approval rules that can be set at an instance level are:
- **Prevent approval of merge requests by merge request author**. Prevents project
- maintainers from allowing request authors to merge their own merge requests.
+maintainers from allowing request authors to merge their own merge requests.
- **Prevent approval of merge requests by merge request committers**. Prevents project
- maintainers from allowing users to approve merge requests if they have submitted
- any commits to the source branch.
-- **Prevent users from modifying merge request approvers list**. Prevents project
- maintainers from allowing users to modify the approvers list in project settings
- or in individual merge requests.
-
-## Scope rules to compliance-labeled projects
-
-> Introduced in [GitLab Premium](https://gitlab.com/groups/gitlab-org/-/epics/3432) 13.2.
-
-Merge request approval rules can be further scoped to specific compliance frameworks.
-
-When the compliance framework label is selected and the project is assigned the compliance
-label, the instance-level MR approval settings will take effect and the
-[project-level settings](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule)
-is locked for modification.
-
-When the compliance framework label is not selected or the project is not assigned the
-compliance label, the project-level MR approval settings will take effect and the users with
-Maintainer role and above can modify these.
-
-| Instance-level | Project-level |
-| -------------- | ------------- |
-| ![Scope MR approval settings to compliance frameworks](img/scope_mr_approval_settings_v13_1.png) | ![MR approval settings on compliance projects](img/mr_approval_settings_compliance_project_v13_1.png) |
+maintainers from allowing users to approve merge requests if they have submitted
+any commits to the source branch.
+- **Prevent users from modifying merge request approvers list**. Prevents users from
+modifying the approvers list in project settings or in individual merge requests.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2eff8d1365f..624e0aa9d57 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6817,9 +6817,6 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
-msgid "Compliance frameworks"
-msgstr ""
-
msgid "ComplianceDashboard|created by:"
msgstr ""
@@ -21738,9 +21735,6 @@ msgstr ""
msgid "Registry setup"
msgstr ""
-msgid "Regulate approvals by authors/committers, based on compliance frameworks. Can be changed only at the instance level."
-msgstr ""
-
msgid "Reindexing status"
msgstr ""
@@ -26064,9 +26058,6 @@ msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
-msgid "The above settings apply to all projects with the selected compliance framework(s)."
-msgstr ""
-
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
msgstr ""
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 3f32209543f..38f4ce54e5c 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -25,6 +25,20 @@ RSpec.describe Admin::GroupsController do
end
end
+ describe 'POST #create' do
+ it 'creates group' do
+ expect do
+ post :create, params: { group: { path: 'test', name: 'test' } }
+ end.to change { Group.count }.by(1)
+ end
+
+ it 'creates namespace_settings for group' do
+ expect do
+ post :create, params: { group: { path: 'test', name: 'test' } }
+ end.to change { NamespaceSetting.count }.by(1)
+ end
+ end
+
describe 'PUT #members_update' do
let(:group_user) { create(:user) }
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index a9350bc059d..e8a126d8774 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -1,9 +1,14 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import {
+ GlIntersectionObserver,
+ GlButton,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlLink,
+} from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
@@ -88,20 +93,25 @@ describe('DropdownContentsLabelsView', () => {
});
});
- describe('showListContainer', () => {
+ describe('showNoMatchingResultsMessage', () => {
it.each`
- variant | loading | showList
- ${'sidebar'} | ${false} | ${true}
- ${'sidebar'} | ${true} | ${false}
- ${'not-sidebar'} | ${true} | ${true}
- ${'not-sidebar'} | ${false} | ${true}
+ searchKey | labels | labelsDescription | returnValue
+ ${''} | ${[]} | ${'empty'} | ${false}
+ ${'bug'} | ${[]} | ${'empty'} | ${true}
+ ${''} | ${mockLabels} | ${'not empty'} | ${false}
+ ${'bug'} | ${mockLabels} | ${'not empty'} | ${false}
`(
- 'returns $showList if `state.variant` is "$variant" and `labelsFetchInProgress` is $loading',
- ({ variant, loading, showList }) => {
- createComponent({ ...mockConfig, variant });
- wrapper.vm.$store.state.labelsFetchInProgress = loading;
+ 'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription',
+ async ({ searchKey, labels, returnValue }) => {
+ wrapper.setData({
+ searchKey,
+ });
- expect(wrapper.vm.showListContainer).toBe(showList);
+ wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue);
},
);
});
@@ -118,6 +128,28 @@ describe('DropdownContentsLabelsView', () => {
});
});
+ describe('handleComponentDisappear', () => {
+ it('calls action `receiveLabelsSuccess` with empty array', () => {
+ jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
+
+ wrapper.vm.handleComponentDisappear();
+
+ expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
+ });
+ });
+
+ describe('handleCreateLabelClick', () => {
+ it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', () => {
+ jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
+ jest.spyOn(wrapper.vm, 'toggleDropdownContentsCreateView');
+
+ wrapper.vm.handleCreateLabelClick();
+
+ expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
+ expect(wrapper.vm.toggleDropdownContentsCreateView).toHaveBeenCalled();
+ });
+ });
+
describe('handleKeyDown', () => {
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
wrapper.setData({
@@ -226,8 +258,8 @@ describe('DropdownContentsLabelsView', () => {
});
describe('template', () => {
- it('renders component container element with class `labels-select-contents-list`', () => {
- expect(wrapper.attributes('class')).toContain('labels-select-contents-list');
+ it('renders gl-intersection-observer as component root', () => {
+ expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
});
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
@@ -272,15 +304,11 @@ describe('DropdownContentsLabelsView', () => {
expect(searchInputEl.attributes('autofocus')).toBe('true');
});
- it('renders smart-virtual-list element', () => {
- expect(wrapper.find(SmartVirtualList).exists()).toBe(true);
- });
-
it('renders label elements for all labels', () => {
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
});
- it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
+ it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => {
wrapper.setData({
currentHighlightItem: 0,
});
@@ -288,7 +316,7 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const labelItemEl = findDropdownContent().find(LabelItem);
- expect(labelItemEl.props('highlight')).toBe(true);
+ expect(labelItemEl.attributes('highlight')).toBe('true');
});
});
@@ -310,9 +338,12 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const dropdownContent = findDropdownContent();
+ const loadingIcon = findLoadingIcon();
expect(dropdownContent.exists()).toBe(true);
- expect(dropdownContent.isVisible()).toBe(false);
+ expect(dropdownContent.isVisible()).toBe(true);
+ expect(loadingIcon.exists()).toBe(true);
+ expect(loadingIcon.isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
index ad3f073fdf9..a6ec01ad7e1 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
@@ -6,11 +6,15 @@ import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
-const createComponent = ({ label = mockLabel, highlight = true } = {}) =>
+const createComponent = ({
+ label = mockLabel,
+ isLabelSet = mockLabel.set,
+ highlight = true,
+} = {}) =>
shallowMount(LabelItem, {
propsData: {
label,
- isLabelSet: label.set,
+ isLabelSet,
highlight,
},
});
@@ -26,94 +30,44 @@ describe('LabelItem', () => {
wrapper.destroy();
});
- describe('computed', () => {
- describe('labelBoxStyle', () => {
- it('returns an object containing `backgroundColor` based on `label` prop', () => {
- expect(wrapper.vm.labelBoxStyle).toEqual(
- expect.objectContaining({
- backgroundColor: mockLabel.color,
- }),
- );
- });
- });
- });
-
- describe('watchers', () => {
- describe('isLabelSet', () => {
- it('sets value of `isLabelSet` to `isSet` data prop', () => {
- expect(wrapper.vm.isSet).toBe(true);
-
- wrapper.setProps({
- isLabelSet: false,
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.isSet).toBe(false);
- });
- });
- });
- });
-
- describe('methods', () => {
- describe('handleClick', () => {
- it('sets value of `isSet` data prop to opposite of its current value', () => {
- wrapper.setData({
- isSet: true,
- });
-
- wrapper.vm.handleClick();
- expect(wrapper.vm.isSet).toBe(false);
- wrapper.vm.handleClick();
- expect(wrapper.vm.isSet).toBe(true);
- });
-
- it('emits event `clickLabel` on component with `label` prop as param', () => {
- wrapper.vm.handleClick();
-
- expect(wrapper.emitted('clickLabel')).toBeTruthy();
- expect(wrapper.emitted('clickLabel')[0]).toEqual([mockLabel]);
- });
- });
- });
-
describe('template', () => {
it('renders gl-link component', () => {
expect(wrapper.find(GlLink).exists()).toBe(true);
});
- it('renders gl-link component with class `is-focused` when `highlight` prop is true', () => {
- wrapper.setProps({
+ it('renders component root with class `is-focused` when `highlight` prop is true', () => {
+ const wrapperTemp = createComponent({
highlight: true,
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(GlLink).classes()).toContain('is-focused');
- });
+ expect(wrapperTemp.classes()).toContain('is-focused');
+
+ wrapperTemp.destroy();
});
- it('renders visible gl-icon component when `isSet` prop is true', () => {
- wrapper.setData({
- isSet: true,
+ it('renders visible gl-icon component when `isLabelSet` prop is true', () => {
+ const wrapperTemp = createComponent({
+ isLabelSet: true,
});
- return wrapper.vm.$nextTick(() => {
- const iconEl = wrapper.find(GlIcon);
+ const iconEl = wrapperTemp.find(GlIcon);
- expect(iconEl.isVisible()).toBe(true);
- expect(iconEl.props('name')).toBe('mobile-issue-close');
- });
+ expect(iconEl.isVisible()).toBe(true);
+ expect(iconEl.props('name')).toBe('mobile-issue-close');
+
+ wrapperTemp.destroy();
});
- it('renders visible span element as placeholder instead of gl-icon when `isSet` prop is false', () => {
- wrapper.setData({
- isSet: false,
+ it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => {
+ const wrapperTemp = createComponent({
+ isLabelSet: false,
});
- return wrapper.vm.$nextTick(() => {
- const placeholderEl = wrapper.find('[data-testid="no-icon"]');
+ const placeholderEl = wrapperTemp.find('[data-testid="no-icon"]');
- expect(placeholderEl.isVisible()).toBe(true);
- });
+ expect(placeholderEl.isVisible()).toBe(true);
+
+ wrapperTemp.destroy();
});
it('renders label color element', () => {
diff --git a/spec/migrations/ensure_namespace_settings_creation_spec.rb b/spec/migrations/ensure_namespace_settings_creation_spec.rb
new file mode 100644
index 00000000000..8574063f7fe
--- /dev/null
+++ b/spec/migrations/ensure_namespace_settings_creation_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20201104124300_ensure_namespace_settings_creation.rb')
+
+RSpec.describe EnsureNamespaceSettingsCreation do
+ context 'when there are namespaces without namespace settings' do
+ let(:namespaces) { table(:namespaces) }
+ let(:namespace_settings) { table(:namespace_settings) }
+ let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
+ let!(:namespace_2) { namespaces.create!(name: 'gitlab', path: 'gitlab-org2') }
+
+ it 'migrates namespaces without namespace_settings' do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(2.minutes.to_i, namespace.id, namespace_2.id)
+ end
+ end
+ end
+
+ it 'schedules migrations in batches ' do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ namespace_3 = namespaces.create!(name: 'gitlab', path: 'gitlab-org3')
+ namespace_4 = namespaces.create!(name: 'gitlab', path: 'gitlab-org4')
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(2.minutes.to_i, namespace.id, namespace_2.id)
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(4.minutes.to_i, namespace_3.id, namespace_4.id)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb b/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
index cd2ec81abb7..5c3ca281d49 100644
--- a/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
+++ b/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
@@ -30,41 +30,23 @@ RSpec.describe MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord, schema:
subject { described_class.new.up }
- context 'when Gitlab.ee? is true' do
- before do
- expect(Gitlab).to receive(:ee?).and_return(true)
- end
+ it 'updates the project settings' do
+ subject
- it 'updates the project settings' do
- subject
+ gdpr_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'GDPR')
+ expect(project_on_root_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
+ expect(project_on_sub_sub_level_compliance_setting_2.reload.framework_id).to eq(gdpr_framework.id)
- gdpr_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'GDPR')
- expect(project_on_root_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
- expect(project_on_sub_sub_level_compliance_setting_2.reload.framework_id).to eq(gdpr_framework.id)
+ sox_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'SOX')
+ expect(project_on_sub_sub_level_compliance_setting_1.reload.framework_id).to eq(sox_framework.id)
- sox_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'SOX')
- expect(project_on_sub_sub_level_compliance_setting_1.reload.framework_id).to eq(sox_framework.id)
-
- gdpr_framework = compliance_management_frameworks.find_by(namespace_id: namespace.id, name: 'GDPR')
- expect(project_on_namespace_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
- end
-
- it 'adds two framework records' do
- subject
-
- expect(compliance_management_frameworks.count).to eq(3)
- end
+ gdpr_framework = compliance_management_frameworks.find_by(namespace_id: namespace.id, name: 'GDPR')
+ expect(project_on_namespace_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
end
- context 'when Gitlab.ee? is false' do
- before do
- expect(Gitlab).to receive(:ee?).and_return(false)
- end
-
- it 'does nothing' do
- subject
+ it 'adds two framework records' do
+ subject
- expect(compliance_management_frameworks.count).to eq(0)
- end
+ expect(compliance_management_frameworks.count).to eq(3)
end
end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index e03e75653ff..f0fd243f0ca 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
end
end
+ context 'when exception happens' do
+ it 'handles OpenSSL::Cipher::CipherError' do
+ expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
+
+ expect { service.execute }.not_to raise_exception
+ end
+
+ it 'ensures rollback when OpenSSL::Cipher::CipherError' do
+ expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
+ expect(service).to receive(:rollback_folder_move).and_call_original
+
+ service.execute
+ project.reload
+
+ expect(project.legacy_storage?).to be_truthy
+ expect(project.repository_read_only?).to be_falsey
+ end
+
+ it 'handles Gitlab::Git::CommandError' do
+ expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
+
+ expect { service.execute }.not_to raise_exception
+ end
+
+ it 'ensures rollback when Gitlab::Git::CommandError' do
+ expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
+ expect(service).to receive(:rollback_folder_move).and_call_original
+
+ service.execute
+ project.reload
+
+ expect(project.legacy_storage?).to be_truthy
+ expect(project.repository_read_only?).to be_falsey
+ end
+ end
+
context 'when one move fails' do
it 'rollsback repositories to original name' do
allow(service).to receive(:move_repository).and_call_original
diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
index f2b1ce30a54..492eb0956aa 100644
--- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab
end
end
+ context 'when exception happens' do
+ it 'handles OpenSSL::Cipher::CipherError' do
+ expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
+
+ expect { service.execute }.not_to raise_exception
+ end
+
+ it 'ensures rollback when OpenSSL::Cipher::CipherError' do
+ expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
+ expect(service).to receive(:rollback_folder_move).and_call_original
+
+ service.execute
+ project.reload
+
+ expect(project.hashed_storage?(:repository)).to be_truthy
+ expect(project.repository_read_only?).to be_falsey
+ end
+
+ it 'handles Gitlab::Git::CommandError' do
+ expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
+
+ expect { service.execute }.not_to raise_exception
+ end
+
+ it 'ensures rollback when Gitlab::Git::CommandError' do
+ expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
+ expect(service).to receive(:rollback_folder_move).and_call_original
+
+ service.execute
+ project.reload
+
+ expect(project.hashed_storage?(:repository)).to be_truthy
+ expect(project.repository_read_only?).to be_falsey
+ end
+ end
+
context 'when one move fails' do
it 'rolls repositories back to original name' do
allow(service).to receive(:move_repository).and_call_original