summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-12 15:10:33 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-12 15:10:33 +0000
commit8ff63012e9b7e3dc2279e636868af9a438d1fa93 (patch)
tree4dd67f247345cbc2e8629c4a5f2b935dafc988e3
parentef7cfec30c9fab7b9e757877c472ca7ca2eccc2d (diff)
downloadgitlab-ce-8ff63012e9b7e3dc2279e636868af9a438d1fa93.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js5
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue1
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue19
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js29
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js13
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js19
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js1
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue54
-rw-r--r--app/assets/javascripts/static_site_editor/components/skeleton_loader.vue19
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue101
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue38
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js32
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/services_helper.rb2
-rw-r--r--app/helpers/system_note_helper.rb2
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/protected_ref_access.rb4
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/sent_notification.rb2
-rw-r--r--app/policies/ci/build_policy.rb10
-rw-r--r--app/serializers/analytics_summary_entity.rb4
-rw-r--r--app/services/branches/create_service.rb2
-rw-r--r--app/workers/all_queues.yml14
-rw-r--r--app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb1
-rw-r--r--changelogs/unreleased/35069-protect-builds.yml5
-rw-r--r--changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml5
-rw-r--r--changelogs/unreleased/fix-cop-inject-multiple.yml5
-rw-r--r--changelogs/unreleased/fix-subgroup-milestone-links.yml5
-rw-r--r--changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml5
-rw-r--r--config/initializers/rack_attack_new.rb2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb43
-rw-r--r--doc/api/labels.md2
-rw-r--r--doc/ci/caching/index.md5
-rw-r--r--doc/user/permissions.md4
-rw-r--r--doc/user/project/merge_requests/versions.md22
-rw-r--r--lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb9
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb5
-rw-r--r--lib/gitlab/cycle_analytics/summary/commit.rb6
-rw-r--r--lib/gitlab/cycle_analytics/summary/deploy.rb16
-rw-r--r--lib/gitlab/cycle_analytics/summary/deployment_frequency.rb3
-rw-r--r--lib/gitlab/cycle_analytics/summary/issue.rb11
-rw-r--r--lib/gitlab/cycle_analytics/summary/value.rb42
-rw-r--r--lib/gitlab/cycle_analytics/summary_helper.rb5
-rw-r--r--lib/gitlab/url_builder.rb3
-rw-r--r--locale/gitlab.pot24
-rw-r--r--rubocop/cop/inject_enterprise_edition_module.rb10
-rw-r--r--spec/frontend/diffs/store/getters_versions_dropdowns_spec.js9
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap1
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js8
-rw-r--r--spec/frontend/monitoring/mock_data.js2
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js44
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js46
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js43
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js79
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js182
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js21
-rw-r--r--spec/helpers/milestones_helper_spec.rb15
-rw-r--r--spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/summary/value_spec.rb29
-rw-r--r--spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb81
-rw-r--r--spec/models/merge_request_spec.rb38
-rw-r--r--spec/models/sent_notification_spec.rb22
-rw-r--r--spec/policies/ci/build_policy_spec.rb18
-rw-r--r--spec/requests/api/branches_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/branches/create_spec.rb2
-rw-r--r--spec/rubocop/cop/inject_enterprise_edition_module_spec.rb11
-rw-r--r--spec/services/branches/create_service_spec.rb30
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/C++.gitignore0
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/Java.gitignore0
79 files changed, 968 insertions, 544 deletions
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
index acc8874dad8..1e8e736c028 100644
--- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -40,10 +40,7 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
};
};
- if (gon.features?.diffCompareWithHead) {
- return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
- }
- return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
+ return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
};
export const diffCompareDropdownSourceVersions = (state, getters) => {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2b5a10907f5..0a6329fbdfb 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -440,7 +440,6 @@ export default {
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
- :selected-dashboard="selectedDashboard"
@selectDashboard="selectDashboard($event)"
/>
</div>
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 9ef4b93d543..8b86890715f 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions, mapGetters } from 'vuex';
import {
GlAlert,
GlIcon,
@@ -36,11 +36,6 @@ export default {
GlModal: GlModalDirective,
},
props: {
- selectedDashboard: {
- type: Object,
- required: false,
- default: () => ({}),
- },
defaultBranch: {
type: String,
required: true,
@@ -56,11 +51,15 @@ export default {
},
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
+ ...mapGetters('monitoringDashboard', ['selectedDashboard']),
isSystemDashboard() {
- return this.selectedDashboard.system_dashboard;
+ return this.selectedDashboard?.system_dashboard;
},
selectedDashboardText() {
- return this.selectedDashboard.display_name;
+ return this.selectedDashboard?.display_name;
+ },
+ selectedDashboardPath() {
+ return this.selectedDashboard?.path;
},
filteredDashboards() {
@@ -145,7 +144,7 @@ export default {
<gl-dropdown-item
v-for="dashboard in starredDashboards"
:key="dashboard.path"
- :active="dashboard.path === selectedDashboard.path"
+ :active="dashboard.path === selectedDashboardPath"
active-class="is-active"
@click="selectDashboard(dashboard)"
>
@@ -163,7 +162,7 @@ export default {
<gl-dropdown-item
v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
- :active="dashboard.path === selectedDashboard.path"
+ :active="dashboard.path === selectedDashboardPath"
active-class="is-active"
@click="selectDashboard(dashboard)"
>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 903105babeb..4481395cbbd 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -345,6 +345,35 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
// Dashboard manipulation
+export const toggleStarredValue = ({ commit, state, getters }) => {
+ const { selectedDashboard } = getters;
+
+ if (state.isUpdatingStarredValue) {
+ // Prevent repeating requests for the same change
+ return;
+ }
+ if (!selectedDashboard) {
+ return;
+ }
+
+ const method = selectedDashboard.starred ? 'DELETE' : 'POST';
+ const url = selectedDashboard.user_starred_path;
+ const newStarredValue = !selectedDashboard.starred;
+
+ commit(types.REQUEST_DASHBOARD_STARRING);
+
+ axios({
+ url,
+ method,
+ })
+ .then(() => {
+ commit(types.RECEIVE_DASHBOARD_STARRING_SUCCESS, newStarredValue);
+ })
+ .catch(() => {
+ commit(types.RECEIVE_DASHBOARD_STARRING_FAILURE);
+ });
+};
+
/**
* Set a new array of metrics to a panel group
* @param {*} data An object containing
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 1cadc287204..8f8dc8127a0 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -4,6 +4,19 @@ const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
/**
+ * Returns a reference to the currently selected dashboard
+ * from the list of dashboards.
+ *
+ * @param {Object} state
+ */
+export const selectedDashboard = state => {
+ const { allDashboards } = state;
+ return (
+ allDashboards.find(({ path }) => path === state.currentDashboard) || allDashboards[0] || null
+ );
+};
+
+/**
* Get all state for metric in the dashboard or a group. The
* states are not repeated so the dashboard or group can show
* a global state.
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 2fd0efa4ab7..dc78e50476e 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -5,6 +5,10 @@ export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAIL
export const SET_PROM_QUERY_VARIABLES = 'SET_PROM_QUERY_VARIABLES';
export const UPDATE_VARIABLE_DATA = 'UPDATE_VARIABLE_DATA';
+export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING';
+export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
+export const RECEIVE_DASHBOARD_STARRING_FAILURE = 'RECEIVE_DASHBOARD_STARRING_FAILURE';
+
// Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 8de1430302a..ec519ee4576 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,5 +1,7 @@
+import Vue from 'vue';
import { pick } from 'lodash';
import * as types from './mutation_types';
+import { selectedDashboard } from './getters';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { endpointKeys, initialStateKeys, metricStates } from '../constants';
@@ -71,6 +73,23 @@ export default {
state.showEmptyState = true;
},
+ [types.REQUEST_DASHBOARD_STARRING](state) {
+ state.isUpdatingStarredValue = true;
+ },
+ [types.RECEIVE_DASHBOARD_STARRING_SUCCESS](state, newStarredValue) {
+ const dashboard = selectedDashboard(state);
+ const index = state.allDashboards.findIndex(d => d === dashboard);
+
+ state.isUpdatingStarredValue = false;
+
+ // Trigger state updates in the reactivity system for this change
+ // https://vuejs.org/v2/guide/reactivity.html#For-Arrays
+ Vue.set(state.allDashboards, index, { ...dashboard, starred: newStarredValue });
+ },
+ [types.RECEIVE_DASHBOARD_STARRING_FAILURE](state) {
+ state.isUpdatingStarredValue = false;
+ },
+
/**
* Deployments and environments
*/
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index f1b2baf0f74..9ae1da93e5f 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -14,6 +14,7 @@ export default () => ({
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
+ isUpdatingStarredValue: false,
dashboard: {
panelGroups: [],
},
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 921d93669c5..aafa5a2d0ba 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -1,18 +1,68 @@
<script>
import { GlFormTextarea } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+import PublishToolbar from '../components/publish_toolbar.vue';
+import EditHeader from '../components/edit_header.vue';
+
export default {
components: {
GlFormTextarea,
+ RichContentEditor,
+ PublishToolbar,
+ EditHeader,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
- value: {
+ title: {
type: String,
required: true,
},
+ content: {
+ type: String,
+ required: true,
+ },
+ savingChanges: {
+ type: Boolean,
+ required: true,
+ },
+ returnUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ editableContent: this.content,
+ saveable: false,
+ };
+ },
+ computed: {
+ modified() {
+ return this.content !== this.editableContent;
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$emit('submit', { content: this.editableContent });
+ },
},
};
</script>
<template>
- <gl-form-textarea :value="value" v-on="$listeners" />
+ <div class="d-flex flex-grow-1 flex-column">
+ <edit-header class="py-2" :title="title" />
+ <rich-content-editor v-if="glFeatures.richContentEditor" v-model="editableContent" />
+ <gl-form-textarea v-else v-model="editableContent" class="h-100 shadow-none" />
+ <publish-toolbar
+ class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
+ :return-url="returnUrl"
+ :saveable="modified"
+ :saving-changes="savingChanges"
+ @submit="onSubmit"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue b/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue
new file mode 100644
index 00000000000..1b6179883aa
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue
@@ -0,0 +1,19 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :width="500" :height="102">
+ <rect width="500" height="16" rx="4" />
+ <rect y="20" width="375" height="16" rx="4" />
+ <rect x="380" y="20" width="120" height="16" rx="4" />
+ <rect y="40" width="250" height="16" rx="4" />
+ <rect x="255" y="40" width="150" height="16" rx="4" />
+ <rect x="410" y="40" width="90" height="16" rx="4" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 9003c381456..e48e8b5d23c 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -1,13 +1,9 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+import SkeletonLoader from '../components/skeleton_loader.vue';
import EditArea from '../components/edit_area.vue';
-import EditHeader from '../components/edit_header.vue';
import SavedChangesMessage from '../components/saved_changes_message.vue';
-import PublishToolbar from '../components/publish_toolbar.vue';
import InvalidContentMessage from '../components/invalid_content_message.vue';
import SubmitChangesError from '../components/submit_changes_error.vue';
@@ -20,16 +16,12 @@ import { LOAD_CONTENT_ERROR } from '../constants';
export default {
components: {
- RichContentEditor,
+ SkeletonLoader,
EditArea,
- EditHeader,
InvalidContentMessage,
- GlSkeletonLoader,
SavedChangesMessage,
- PublishToolbar,
SubmitChangesError,
},
- mixins: [glFeatureFlagsMixin()],
apollo: {
appData: {
query: appDataQuery,
@@ -58,80 +50,51 @@ export default {
},
},
computed: {
- ...mapState([
- 'content',
- 'isLoadingContent',
- 'isSavingChanges',
- 'isContentLoaded',
- 'returnUrl',
- 'title',
- 'submitChangesError',
- 'savedContentMeta',
- ]),
- ...mapGetters(['contentChanged']),
- },
- mounted() {
- if (this.appData.isSupportedContent) {
- this.loadContent();
- }
+ ...mapState(['isSavingChanges', 'submitChangesError', 'savedContentMeta']),
+ isLoadingContent() {
+ return this.$apollo.queries.sourceContent.loading;
+ },
+ isContentLoaded() {
+ return Boolean(this.sourceContent);
+ },
},
methods: {
- ...mapActions(['loadContent', 'setContent', 'submitChanges', 'dismissSubmitChangesError']),
+ ...mapActions(['setContent', 'submitChanges', 'dismissSubmitChangesError']),
+ onSubmit({ content }) {
+ this.setContent(content);
+ this.submitChanges();
+ },
},
};
</script>
<template>
- <div class="d-flex justify-content-center h-100 pt-2">
+ <div class="container d-flex gl-flex-direction-column pt-2 h-100">
<!-- Success view -->
<saved-changes-message
v-if="savedContentMeta"
- class="w-75"
:branch="savedContentMeta.branch"
:commit="savedContentMeta.commit"
:merge-request="savedContentMeta.mergeRequest"
- :return-url="returnUrl"
+ :return-url="appData.returnUrl"
/>
<!-- Main view -->
<template v-else-if="appData.isSupportedContent">
- <div v-if="isLoadingContent" class="w-50 h-50">
- <gl-skeleton-loader :width="500" :height="102">
- <rect width="500" height="16" rx="4" />
- <rect y="20" width="375" height="16" rx="4" />
- <rect x="380" y="20" width="120" height="16" rx="4" />
- <rect y="40" width="250" height="16" rx="4" />
- <rect x="255" y="40" width="150" height="16" rx="4" />
- <rect x="410" y="40" width="90" height="16" rx="4" />
- </gl-skeleton-loader>
- </div>
- <div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
- <submit-changes-error
- v-if="submitChangesError"
- class="w-75 align-self-center"
- :error="submitChangesError"
- @retry="submitChanges"
- @dismiss="dismissSubmitChangesError"
- />
- <edit-header class="w-75 align-self-center py-2" :title="title" />
- <rich-content-editor
- v-if="glFeatures.richContentEditor"
- class="w-75 gl-align-self-center"
- :value="content"
- @input="setContent"
- />
- <edit-area
- v-else
- class="w-75 h-100 shadow-none align-self-center"
- :value="content"
- @input="setContent"
- />
- <publish-toolbar
- :return-url="returnUrl"
- :saveable="contentChanged"
- :saving-changes="isSavingChanges"
- @submit="submitChanges"
- />
- </div>
+ <skeleton-loader v-if="isLoadingContent" class="w-75 gl-align-self-center gl-mt-5" />
+ <submit-changes-error
+ v-if="submitChangesError"
+ :error="submitChangesError"
+ @retry="submitChanges"
+ @dismiss="dismissSubmitChangesError"
+ />
+ <edit-area
+ v-if="isContentLoaded"
+ :title="sourceContent.title"
+ :content="sourceContent.content"
+ :saving-changes="isSavingChanges"
+ :return-url="appData.returnUrl"
+ @submit="onSubmit"
+ />
</template>
<!-- Error view -->
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index 2730e8ee1da..f45c14f8344 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -27,9 +27,9 @@ export default {
class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
@click="handleButtonClick"
>
- <span class="dropdown-toggle-text" :class="{ 'flex-fill': isDropdownVariantStandalone }">{{
- dropdownButtonText
- }}</span>
+ <span class="dropdown-toggle-text flex-fill">
+ {{ dropdownButtonText }}
+ </span>
<gl-icon name="chevron-down" class="pull-right" />
</gl-button>
</template>
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 a548ef4ac86..1ef2e8b3bed 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,16 +1,18 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon, GlButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import LabelItem from './label_item.vue';
+
export default {
components: {
GlLoadingIcon,
GlButton,
- GlIcon,
GlSearchBoxByType,
GlLink,
+ LabelItem,
},
data() {
return {
@@ -60,11 +62,6 @@ export default {
'updateSelectedLabels',
'toggleDropdownContents',
]),
- getDropdownLabelBoxStyle(label) {
- return {
- backgroundColor: label.color,
- };
- },
isLabelSelected(label) {
return this.selectedLabelsList.includes(label.id);
},
@@ -138,24 +135,19 @@ export default {
@click="toggleDropdownContents"
/>
</div>
- <div class="dropdown-input">
+ <div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
</div>
- <div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
+ <div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
<ul class="list-unstyled mb-0">
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
- <gl-link
- class="d-flex align-items-baseline text-break-word label-item"
- :class="{ 'is-focused': index === currentHighlightItem }"
- @click="handleLabelClick(label)"
- >
- <gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" />
- <span v-show="!label.set" class="mr-3 pr-2"></span>
- <span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span>
- <span>{{ label.title }}</span>
- </gl-link>
+ <label-item
+ :label="label"
+ :highlight="index === currentHighlightItem"
+ @clickLabel="handleLabelClick(label)"
+ />
</li>
- <li v-if="!visibleLabels.length" class="p-2 text-center">
+ <li v-show="!visibleLabels.length" class="p-2 text-center">
{{ __('No matching results') }}
</li>
</ul>
@@ -170,9 +162,9 @@ export default {
>
</li>
<li>
- <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
- {{ footerManageLabelTitle }}
- </gl-link>
+ <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">{{
+ footerManageLabelTitle
+ }}</gl-link>
</li>
</ul>
</div>
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
new file mode 100644
index 00000000000..c95221d71b5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ label: {
+ type: Object,
+ required: true,
+ },
+ highlight: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isSet: this.label.set,
+ };
+ },
+ computed: {
+ labelBoxStyle() {
+ return {
+ backgroundColor: this.label.color,
+ };
+ },
+ },
+ methods: {
+ handleClick() {
+ this.isSet = !this.isSet;
+ this.$emit('clickLabel', this.label);
+ },
+ },
+};
+</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/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index b55a58850a1..54f8c78b4e1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -58,29 +58,13 @@ export default {
},
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
- // Iterate over all the labels and update
- // `set` prop value to represent their current state.
- const labelIds = labels.map(label => label.id);
- state.labels = state.labels.reduce((allLabels, label) => {
- if (labelIds.includes(label.id)) {
- allLabels.push({
- ...label,
- touched: true,
- set: !label.set,
- });
- } else {
- // In case multiselect is not allowed
- // we unselect any existing selected label
- const unchangedLabel = state.allowMultiselect
- ? label
- : {
- ...label,
- touched: true,
- set: false,
- };
- allLabels.push(unchangedLabel);
- }
- return allLabels;
- }, []);
+ // Find the label to update from all the labels
+ // and change `set` prop value to represent their current state.
+ const labelId = labels.pop()?.id;
+ const candidateLabel = state.labels.find(label => labelId === label.id);
+ if (candidateLabel) {
+ candidateLabel.touched = true;
+ candidateLabel.set = !candidateLabel.set;
+ }
},
};
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9e6c42ea2be..ae7965a2eae 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -26,7 +26,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:merge_ref_head_comments, @project)
- push_frontend_feature_flag(:diff_compare_with_head, @project)
push_frontend_feature_flag(:accessibility_merge_request_widget, @project)
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index f36a638555a..df8009c3d00 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -367,7 +367,7 @@ module ApplicationSettingsHelper
end
end
-ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper')
# The methods in `EE::ApplicationSettingsHelper` should be available as both
# instance and class methods.
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 12f75217a8a..a57e27d23c8 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -152,7 +152,7 @@ module AuthHelper
extend self
end
-AuthHelper.prepend_if_ee('EE::AuthHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+AuthHelper.prepend_if_ee('EE::AuthHelper')
# The methods added in EE should be available as both class and instance
# methods, just like the methods provided by `AuthHelper` itself.
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 2f5aac892ab..df1ee54c5ac 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -249,7 +249,7 @@ module MilestonesHelper
if milestone.legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
- group_milestone_path(@group, milestone.iid, milestone: params)
+ group_milestone_path(milestone.group, milestone.iid, milestone: params)
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 2151d1a85d7..5c7c128da90 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
module ProjectsHelper
- prepend_if_ee('::EE::ProjectsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def project_incident_management_setting
@project_incident_management_setting ||= @project.incident_management_setting ||
@project.build_incident_management_setting
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index dcdf6bbd3da..b13cc93436f 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -101,7 +101,7 @@ module ServicesHelper
extend self
end
-ServicesHelper.prepend_if_ee('EE::ServicesHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ServicesHelper.prepend_if_ee('EE::ServicesHelper')
# The methods in `EE::ServicesHelper` should be available as both instance and
# class methods.
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index d3b6ecf2bd7..33268b5dbc9 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -42,7 +42,7 @@ module SystemNoteHelper
extend self
end
-SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper')
# The methods in `EE::SystemNoteHelper` should be available as both instance and
# class methods.
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index d1e54d46d40..247ba0822fe 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -150,5 +150,5 @@ end
Noteable.extend(Noteable::ClassMethods)
-Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
+Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods')
Noteable.prepend_if_ee('EE::Noteable')
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 7373f006d64..d1e3d9b2aff 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -50,8 +50,8 @@ module ProtectedRefAccess
end
end
-ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes') # rubocop: disable Cop/InjectEnterpriseEditionModule
-ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes')
+ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess')
# When using `prepend` (or `include` for that matter), the `ClassMethods`
# constants are not merged. This means that `class_methods` in
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 3270c7c131f..7d7788c7e23 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -875,7 +875,7 @@ class MergeRequest < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def diffable_merge_ref?
- Feature.enabled?(:diff_compare_with_head, target_project) && can_be_merged? && merge_ref_head.present?
+ can_be_merged? && merge_ref_head.present?
end
# Returns boolean indicating the merge_status should be rechecked in order to
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 1bfcad02f38..4165d3b753f 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -82,6 +82,8 @@ class SentNotification < ApplicationRecord
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
+ else
+ new_position = nil
end
super(new_position)
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index ebb99270b9a..12892a69257 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -12,6 +12,14 @@ module Ci
end
end
+ condition(:unprotected_ref) do
+ if @subject.tag?
+ !ProtectedTag.protected?(@subject.project, @subject.ref)
+ else
+ !ProtectedBranch.protected?(@subject.project, @subject.ref)
+ end
+ end
+
condition(:owner_of_job) do
@subject.triggered_by?(@user)
end
@@ -34,7 +42,7 @@ module Ci
prevent :erase_build
end
- rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
+ rule { can?(:admin_build) | (can?(:update_build) & owner_of_job & unprotected_ref) }.enable :erase_build
rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_build
diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb
index 57e9225e2da..62828fc1428 100644
--- a/app/serializers/analytics_summary_entity.rb
+++ b/app/serializers/analytics_summary_entity.rb
@@ -8,8 +8,6 @@ class AnalyticsSummaryEntity < Grape::Entity
private
def value
- return object.value if object.value.is_a? String
-
- object.value&.nonzero? ? object.value.to_s : '-'
+ object.value.to_s
end
end
diff --git a/app/services/branches/create_service.rb b/app/services/branches/create_service.rb
index c8afd97e6bf..958dd5c9965 100644
--- a/app/services/branches/create_service.rb
+++ b/app/services/branches/create_service.rb
@@ -14,7 +14,7 @@ module Branches
if new_branch
success(new_branch)
else
- error("Invalid reference name: #{branch_name}")
+ error("Invalid reference name: #{ref}")
end
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 33a20afc0ba..1f9a53d64d9 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -10,6 +10,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent: true
+- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
+ :feature_category: :authentication_and_authorization
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
- :name: auto_devops:auto_devops_disable
:feature_category: :auto_devops
:has_external_dependencies:
@@ -934,13 +941,6 @@
:resource_boundary: :unknown
:weight: 2
:idempotent: true
-- :name: authorized_project_update_user_refresh_with_low_urgency
- :feature_category: :authentication_and_authorization
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :name: authorized_projects
:feature_category: :authentication_and_authorization
:has_external_dependencies:
diff --git a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
index 54025422874..19038cb8900 100644
--- a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
@@ -4,6 +4,7 @@ module AuthorizedProjectUpdate
class UserRefreshWithLowUrgencyWorker < ::AuthorizedProjectsWorker
feature_category :authentication_and_authorization
urgency :low
+ queue_namespace :authorized_project_update
idempotent!
end
diff --git a/changelogs/unreleased/35069-protect-builds.yml b/changelogs/unreleased/35069-protect-builds.yml
new file mode 100644
index 00000000000..7efe4ab529c
--- /dev/null
+++ b/changelogs/unreleased/35069-protect-builds.yml
@@ -0,0 +1,5 @@
+---
+title: Disallow developers to delete builds of protected branches
+merge_request: 28881
+author: Alexander Kutelev
+type: changed
diff --git a/changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml b/changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml
deleted file mode 100644
index ac9ae44b958..00000000000
--- a/changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: backfill environment_id on deployment_merge_requests
-merge_request: 27219
-author:
-type: other
diff --git a/changelogs/unreleased/fix-cop-inject-multiple.yml b/changelogs/unreleased/fix-cop-inject-multiple.yml
new file mode 100644
index 00000000000..8fe1556abac
--- /dev/null
+++ b/changelogs/unreleased/fix-cop-inject-multiple.yml
@@ -0,0 +1,5 @@
+---
+title: Allow multiple usage of EE extension/inclusion on last lines
+merge_request: 31183
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/fix-subgroup-milestone-links.yml b/changelogs/unreleased/fix-subgroup-milestone-links.yml
new file mode 100644
index 00000000000..dab14b33f9e
--- /dev/null
+++ b/changelogs/unreleased/fix-subgroup-milestone-links.yml
@@ -0,0 +1,5 @@
+---
+title: Link to subgroup milestones correctly from group milestones page
+merge_request: 31383
+author:
+type: fixed
diff --git a/changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml b/changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml
new file mode 100644
index 00000000000..c7222c6583d
--- /dev/null
+++ b/changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Allow showing merge request diffs compared to current version of target branch
+merge_request: 31325
+author:
+type: added
diff --git a/config/initializers/rack_attack_new.rb b/config/initializers/rack_attack_new.rb
index 38e581f188f..51b49bec864 100644
--- a/config/initializers/rack_attack_new.rb
+++ b/config/initializers/rack_attack_new.rb
@@ -160,5 +160,5 @@ class Rack::Attack
end
end
-::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack') # rubocop: disable Cop/InjectEnterpriseEditionModule
+::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack')
::Rack::Attack::Request.prepend_if_ee('::EE::Gitlab::Rack::Attack::Request')
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 960a2eeae10..e6e0b4b4409 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -34,8 +34,6 @@
- 2
- - authorized_project_update
- 1
-- - authorized_project_update_user_refresh_with_low_urgency
- - 1
- - authorized_projects
- 2
- - auto_devops
diff --git a/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb b/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
index 24b652a3299..77cb1ae8508 100644
--- a/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
+++ b/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
@@ -4,46 +4,19 @@ class BackfillEnvironmentIdOnDeploymentMergeRequests < ActiveRecord::Migration[6
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
- BATCH_SIZE = 400
- DELAY = 1.minute
disable_ddl_transaction!
def up
- max_mr_id = DeploymentMergeRequest
- .select(:merge_request_id)
- .distinct
- .order(merge_request_id: :desc)
- .limit(1)
- .pluck(:merge_request_id)
- .first || 0
-
- last_mr_id = 0
- step = 0
-
- while last_mr_id < max_mr_id
- stop =
- DeploymentMergeRequest
- .select(:merge_request_id)
- .distinct
- .where('merge_request_id > ?', last_mr_id)
- .order(:merge_request_id)
- .offset(BATCH_SIZE)
- .limit(1)
- .pluck(:merge_request_id)
- .first
-
- stop ||= max_mr_id
-
- migrate_in(
- step * DELAY,
- 'BackfillEnvironmentIdDeploymentMergeRequests',
- [last_mr_id + 1, stop]
- )
+ # no-op
- last_mr_id = stop
- step += 1
- end
+ # this migration is deleted because there is no foreign key for
+ # deployments.environment_id and this caused a failure upgrading
+ # deployments_merge_requests.environment_id
+ #
+ # Details on the following issues:
+ # * https://gitlab.com/gitlab-org/gitlab/-/issues/217191
+ # * https://gitlab.com/gitlab-org/gitlab/-/issues/26229
end
def down
diff --git a/doc/api/labels.md b/doc/api/labels.md
index eb8ec906ec1..3ced7da8ed5 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -7,6 +7,8 @@ The `description_html` - was added to response JSON in [GitLab 12.7](https://git
Get all labels for a given project.
+By default, this request returns 20 results at a time because the API results [are paginated](README.md#pagination).
+
```plaintext
GET /projects/:id/labels
```
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index 12267b4ab9f..16cabae353e 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -39,8 +39,9 @@ runtime dependencies needed to compile the project:
- `artifacts`: **Use for stage results that will be passed between stages.**
Artifacts are files generated by a job which are stored and uploaded, and can then
- be fetched and used by jobs in later stages of the **same pipeline**. This data
- will not be available in different pipelines, but is available to be downloaded
+ be fetched and used by jobs in later stages of the **same pipeline**. In other words,
+ [you can't create an artifact in job-A in stage-1, and then use this artifact in job-B in stage-1](https://gitlab.com/gitlab-org/gitlab/-/issues/25837).
+ This data will not be available in different pipelines, but is available to be downloaded
from the UI.
The name `artifacts` sounds like it's only useful outside of the job, like for downloading
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index f23f1858883..e54063e3eed 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -403,7 +403,9 @@ instance and project. In addition, all admins can use the admin interface under
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
-1. Only if the job was triggered by the user
+1. Only if the job was:
+ - Triggered by the user
+ - [Since GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/35069), not run for a protected branch
### Job permissions
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index 2f51af24a95..84934148bdc 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -57,7 +57,7 @@ source and target branch can be shown mixed together making it hard to
understand which changes are being added and which already exist in the
target branch.
-In GitLab 12.10, we added an **experimental** comparison mode, which
+In GitLab 12.10, we added a comparison mode, which
shows a diff calculated by simulating how it would look like once merged - a more accurate
representation of the changes rather than using the base of the two
branches. The new mode is available from the comparison target drop down
@@ -67,26 +67,6 @@ current default comparison.
![Merge request versions compare HEAD](img/versions_compare_head_v12_10.png)
-### Enable or disable `HEAD` comparison mode **(CORE ONLY)**
-
-`HEAD` comparison mode is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
-can enable it for your instance. You're welcome to test it, but use it at your
-own risk.
-
-To enable it:
-
-```ruby
-Feature.enable(:diff_compare_with_head)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:diff_compare_with_head)
-```
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb b/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
index 67b5854352b..4fd3b81fda3 100644
--- a/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
+++ b/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
@@ -5,7 +5,14 @@ module Gitlab
# BackfillEnvironmentIdDeploymentMergeRequests deletes duplicates
# from deployment_merge_requests table and backfills environment_id
class BackfillEnvironmentIdDeploymentMergeRequests
- def perform(start_mr_id, stop_mr_id)
+ def perform(_start_mr_id, _stop_mr_id)
+ # no-op
+
+ # Background migration removed due to
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/217191
+ end
+
+ def backfill_range(start_mr_id, stop_mr_id)
start_mr_id = Integer(start_mr_id)
stop_mr_id = Integer(stop_mr_id)
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
index 564feb0319f..7559cd376bf 100644
--- a/lib/gitlab/cycle_analytics/stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -28,8 +28,7 @@ module Gitlab
end
def deployments_summary
- @deployments_summary ||=
- Summary::Deploy.new(project: @project, from: @from, to: @to)
+ @deployments_summary ||= Summary::Deploy.new(project: @project, from: @from, to: @to)
end
def deploy_stats
@@ -39,7 +38,7 @@ module Gitlab
def deployment_frequency_stats
serialize(
Summary::DeploymentFrequency.new(
- deployments: deployments_summary.value,
+ deployments: deployments_summary.value.raw_value,
from: @from,
to: @to),
with_unit: true
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
index 76049c6b742..1f426b81800 100644
--- a/lib/gitlab/cycle_analytics/summary/commit.rb
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def value
- @value ||= count_commits
+ @value ||= commits_count ? Value::PrettyNumeric.new(commits_count) : Value::None.new
end
private
@@ -18,10 +18,10 @@ module Gitlab
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
# the easiest way forward is to replicate the relevant portions of the
# `log` function here.
- def count_commits
+ def commits_count
return unless ref
- gitaly_commit_client.commit_count(ref, after: @from, before: @to)
+ @commits_count ||= gitaly_commit_client.commit_count(ref, after: @from, before: @to)
end
def gitaly_commit_client
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
index 5ff8d881143..8544ea1a91e 100644
--- a/lib/gitlab/cycle_analytics/summary/deploy.rb
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -4,18 +4,20 @@ module Gitlab
module CycleAnalytics
module Summary
class Deploy < Base
- include Gitlab::Utils::StrongMemoize
-
def title
n_('Deploy', 'Deploys', value)
end
def value
- strong_memoize(:value) do
- query = @project.deployments.success.where("created_at >= ?", @from)
- query = query.where("created_at <= ?", @to) if @to
- query.count
- end
+ @value ||= Value::PrettyNumeric.new(deployments_count)
+ end
+
+ private
+
+ def deployments_count
+ query = @project.deployments.success.where("created_at >= ?", @from)
+ query = query.where("created_at <= ?", @to) if @to
+ query.count
end
end
end
diff --git a/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb b/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb
index 436dc91bd6b..00676a02a6f 100644
--- a/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb
+++ b/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb
@@ -17,8 +17,7 @@ module Gitlab
end
def value
- @value ||=
- frequency(@deployments, @from, @to || Time.now)
+ @value ||= frequency(@deployments, @from, @to || Time.now)
end
def unit
diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb
index 52892eb5a1a..ce7788590b9 100644
--- a/lib/gitlab/cycle_analytics/summary/issue.rb
+++ b/lib/gitlab/cycle_analytics/summary/issue.rb
@@ -16,7 +16,16 @@ module Gitlab
end
def value
- @value ||= IssuesFinder.new(@current_user, project_id: @project.id, created_after: @from, created_before: @to).execute.count
+ @value ||= Value::PrettyNumeric.new(issues_count)
+ end
+
+ private
+
+ def issues_count
+ IssuesFinder
+ .new(@current_user, project_id: @project.id, created_after: @from, created_before: @to)
+ .execute
+ .count
end
end
end
diff --git a/lib/gitlab/cycle_analytics/summary/value.rb b/lib/gitlab/cycle_analytics/summary/value.rb
new file mode 100644
index 00000000000..9516f9ec974
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/value.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Value
+ attr_reader :value
+
+ def raw_value
+ value
+ end
+
+ def to_s
+ raise NotImplementedError
+ end
+
+ class None < self
+ def to_s
+ '-'
+ end
+ end
+
+ class Numeric < self
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value.zero? ? '0' : value.to_s
+ end
+ end
+
+ class PrettyNumeric < Numeric
+ def to_s
+ # 0 is shown as -
+ value.nonzero? ? super : None.new.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary_helper.rb b/lib/gitlab/cycle_analytics/summary_helper.rb
index 06abcd151d4..3cf9f463024 100644
--- a/lib/gitlab/cycle_analytics/summary_helper.rb
+++ b/lib/gitlab/cycle_analytics/summary_helper.rb
@@ -4,10 +4,11 @@ module Gitlab
module CycleAnalytics
module SummaryHelper
def frequency(count, from, to)
- return count if count.zero?
+ return Summary::Value::None.new if count.zero?
freq = (count / days(from, to)).round(1)
- freq.zero? ? '0' : freq
+
+ Summary::Value::Numeric.new(freq)
end
def days(from, to)
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index a2ebe9f6936..329f87d8be8 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -11,6 +11,9 @@ module Gitlab
class << self
include ActionView::RecordIdentifier
+ # Using a case statement here is preferable for readability and maintainability.
+ # See discussion in https://gitlab.com/gitlab-org/gitlab/-/issues/217397
+ #
# rubocop:disable Metrics/CyclomaticComplexity
def build(object, **options)
# Objects are sometimes wrapped in a BatchLoader instance
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 50a78658c28..ccd972a7037 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3857,15 +3857,6 @@ msgstr ""
msgid "Checkout|(x%{numberOfUsers})"
msgstr ""
-msgid "Checkout|1. Your profile"
-msgstr ""
-
-msgid "Checkout|2. Checkout"
-msgstr ""
-
-msgid "Checkout|3. Your GitLab group"
-msgstr ""
-
msgid "Checkout|Billing address"
msgstr ""
@@ -6513,6 +6504,9 @@ msgstr ""
msgid "Customize your pipeline configuration."
msgstr ""
+msgid "Cycle Time"
+msgstr ""
+
msgid "CycleAnalyticsEvent|Issue closed"
msgstr ""
@@ -17256,6 +17250,18 @@ msgstr ""
msgid "Registration"
msgstr ""
+msgid "Registration|Checkout"
+msgstr ""
+
+msgid "Registration|Your GitLab group"
+msgstr ""
+
+msgid "Registration|Your first project"
+msgstr ""
+
+msgid "Registration|Your profile"
+msgstr ""
+
msgid "Related Deployed Jobs"
msgstr ""
diff --git a/rubocop/cop/inject_enterprise_edition_module.rb b/rubocop/cop/inject_enterprise_edition_module.rb
index 6f007e667f2..7edce94eaee 100644
--- a/rubocop/cop/inject_enterprise_edition_module.rb
+++ b/rubocop/cop/inject_enterprise_edition_module.rb
@@ -17,6 +17,8 @@ module RuboCop
CHECK_LINE_METHODS =
Set.new(%i[include_if_ee extend_if_ee prepend_if_ee]).freeze
+ CHECK_LINE_METHODS_REGEXP = Regexp.union(CHECK_LINE_METHODS.map(&:to_s)).freeze
+
DISALLOW_METHODS = Set.new(%i[include extend prepend]).freeze
def ee_const?(node)
@@ -48,7 +50,13 @@ module RuboCop
# the expression is the last line _of code_.
last_line -= 1 if buffer.source.end_with?("\n")
- add_offense(node, message: INVALID_LINE) if line < last_line
+ last_line_content = buffer.source.split("\n")[-1]
+
+ if CHECK_LINE_METHODS_REGEXP.match?(last_line_content)
+ ignore_node(node)
+ elsif line < last_line
+ add_offense(node, message: INVALID_LINE)
+ end
end
def verify_argument_type(node)
diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
index eb0f2364a50..0343ef75732 100644
--- a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
+++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
@@ -18,7 +18,6 @@ describe('Compare diff version dropdowns', () => {
};
localState.targetBranchName = 'baseVersion';
localState.mergeRequestDiffs = diffsMockData;
- gon.features = { diffCompareWithHead: true };
});
describe('selectedTargetIndex', () => {
@@ -129,14 +128,6 @@ describe('Compare diff version dropdowns', () => {
});
assertVersions(targetVersions);
});
-
- it('does not list head version if feature flag is not enabled', () => {
- gon.features = { diffCompareWithHead: false };
- setupTest();
- const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters);
-
- expect(targetVersions.find(version => version.isHead)).toBeUndefined();
- });
});
it('diffCompareDropdownSourceVersions', () => {
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 6cb7821b341..5770778d8ee 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -16,7 +16,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
- selecteddashboard="[object Object]"
toggle-class="dropdown-menu-toggle"
/>
</div>
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 25b31a793f7..b29d86cbc5b 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -15,6 +15,7 @@ const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starr
describe('DashboardsDropdown', () => {
let wrapper;
let mockDashboards;
+ let mockSelectedDashboard;
function createComponent(props, opts = {}) {
const storeOpts = {
@@ -23,6 +24,7 @@ describe('DashboardsDropdown', () => {
},
computed: {
allDashboards: () => mockDashboards,
+ selectedDashboard: () => mockSelectedDashboard,
},
};
@@ -46,6 +48,7 @@ describe('DashboardsDropdown', () => {
beforeEach(() => {
mockDashboards = dashboardGitResponse;
+ mockSelectedDashboard = null;
});
describe('when it receives dashboards data', () => {
@@ -153,13 +156,12 @@ describe('DashboardsDropdown', () => {
let modalDirective;
beforeEach(() => {
+ [mockSelectedDashboard] = dashboardGitResponse;
modalDirective = jest.fn();
duplicateDashboardAction = jest.fn().mockResolvedValue();
wrapper = createComponent(
- {
- selectedDashboard: dashboardGitResponse[0],
- },
+ {},
{
directives: {
GlModal: modalDirective,
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index dab560d197d..2fa88dfa87a 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -325,6 +325,7 @@ export const dashboardGitResponse = [
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
starred: false,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/common_metrics.yml`,
},
{
default: false,
@@ -334,6 +335,7 @@ export const dashboardGitResponse = [
project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
path: '.gitlab/dashboards/dashboard.yml',
starred: true,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
},
...customDashboardsData,
];
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 901b698b703..44626bfcc57 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -18,6 +18,7 @@ import {
fetchEnvironmentsData,
fetchDashboardData,
fetchAnnotations,
+ toggleStarredValue,
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
@@ -350,6 +351,49 @@ describe('Monitoring store actions', () => {
});
});
+ describe('Toggles starred value of current dashboard', () => {
+ const { state } = store;
+ let unstarredDashboard;
+ let starredDashboard;
+
+ beforeEach(() => {
+ state.isUpdatingStarredValue = false;
+ [unstarredDashboard, starredDashboard] = dashboardGitResponse;
+ });
+
+ describe('toggleStarredValue', () => {
+ it('performs no changes if no dashboard is selected', () => {
+ return testAction(toggleStarredValue, null, state, [], []);
+ });
+
+ it('performs no changes if already changing starred value', () => {
+ state.selectedDashboard = unstarredDashboard;
+ state.isUpdatingStarredValue = true;
+ return testAction(toggleStarredValue, null, state, [], []);
+ });
+
+ it('stars dashboard if it is not starred', () => {
+ state.selectedDashboard = unstarredDashboard;
+ mock.onPost(unstarredDashboard.user_starred_path).reply(200);
+
+ return testAction(toggleStarredValue, null, state, [
+ { type: types.REQUEST_DASHBOARD_STARRING },
+ { type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS, payload: true },
+ ]);
+ });
+
+ it('unstars dashboard if it is starred', () => {
+ state.selectedDashboard = starredDashboard;
+ mock.onPost(starredDashboard.user_starred_path).reply(200);
+
+ return testAction(toggleStarredValue, null, state, [
+ { type: types.REQUEST_DASHBOARD_STARRING },
+ { type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
+ ]);
+ });
+ });
+ });
+
describe('Set initial state', () => {
let mockedState;
beforeEach(() => {
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index e9622071aeb..f07ae4c5a1e 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -3,7 +3,7 @@ import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
-import { environmentData, metricsResult } from '../mock_data';
+import { environmentData, metricsResult, dashboardGitResponse } from '../mock_data';
import {
metricsDashboardPayload,
metricResultStatus,
@@ -350,4 +350,48 @@ describe('Monitoring store Getters', () => {
expect(variablesArray).toEqual([]);
});
});
+
+ describe('selectedDashboard', () => {
+ const { selectedDashboard } = getters;
+
+ it('returns a dashboard', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: dashboardGitResponse[0].path,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns a non-default dashboard', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: dashboardGitResponse[1].path,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[1]);
+ });
+
+ it('returns a default dashboard when no dashboard is selected', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: null,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns a default dashboard when dashboard cannot be found', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: 'wrong_path',
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns null when no dashboards are present', () => {
+ const state = {
+ allDashboards: [],
+ currentDashboard: dashboardGitResponse[0].path,
+ };
+ expect(selectedDashboard(state)).toEqual(null);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index e6564f5e329..29628c99256 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -72,6 +72,49 @@ describe('Monitoring mutations', () => {
});
});
+ describe('Dashboard starring mutations', () => {
+ it('REQUEST_DASHBOARD_STARRING', () => {
+ stateCopy = { isUpdatingStarredValue: false };
+ mutations[types.REQUEST_DASHBOARD_STARRING](stateCopy);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(true);
+ });
+
+ describe('RECEIVE_DASHBOARD_STARRING_SUCCESS', () => {
+ let allDashboards;
+
+ beforeEach(() => {
+ allDashboards = [...dashboardGitResponse];
+ stateCopy = {
+ allDashboards,
+ currentDashboard: allDashboards[1].path,
+ isUpdatingStarredValue: true,
+ };
+ });
+
+ it('sets a dashboard as starred', () => {
+ mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, true);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ expect(stateCopy.allDashboards[1].starred).toBe(true);
+ });
+
+ it('sets a dashboard as unstarred', () => {
+ mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, false);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ expect(stateCopy.allDashboards[1].starred).toBe(false);
+ });
+ });
+
+ it('RECEIVE_DASHBOARD_STARRING_FAILURE', () => {
+ stateCopy = { isUpdatingStarredValue: true };
+ mutations[types.RECEIVE_DASHBOARD_STARRING_FAILURE](stateCopy);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ });
+ });
+
describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
it('stores the deployment data', () => {
stateCopy.deploymentData = [];
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
new file mode 100644
index 00000000000..19330039812
--- /dev/null
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -0,0 +1,79 @@
+import { shallowMount } from '@vue/test-utils';
+
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+
+import EditArea from '~/static_site_editor/components/edit_area.vue';
+import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
+import EditHeader from '~/static_site_editor/components/edit_header.vue';
+
+import { sourceContentTitle as title, sourceContent as content, returnUrl } from '../mock_data';
+
+describe('~/static_site_editor/components/edit_area.vue', () => {
+ let wrapper;
+ const savingChanges = true;
+ const newContent = `new ${content}`;
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = shallowMount(EditArea, {
+ provide: {
+ glFeatures: { richContentEditor: true },
+ },
+ propsData: {
+ title,
+ content,
+ returnUrl,
+ savingChanges,
+ ...propsData,
+ },
+ });
+ };
+
+ const findEditHeader = () => wrapper.find(EditHeader);
+ const findRichContentEditor = () => wrapper.find(RichContentEditor);
+ const findPublishToolbar = () => wrapper.find(PublishToolbar);
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders edit header', () => {
+ expect(findEditHeader().exists()).toBe(true);
+ expect(findEditHeader().props('title')).toBe(title);
+ });
+
+ it('renders rich content editor', () => {
+ expect(findRichContentEditor().exists()).toBe(true);
+ expect(findRichContentEditor().props('value')).toBe(content);
+ });
+
+ it('renders publish toolbar', () => {
+ expect(findPublishToolbar().exists()).toBe(true);
+ expect(findPublishToolbar().props('returnUrl')).toBe(returnUrl);
+ expect(findPublishToolbar().props('savingChanges')).toBe(savingChanges);
+ expect(findPublishToolbar().props('saveable')).toBe(false);
+ });
+
+ describe('when content changes', () => {
+ beforeEach(() => {
+ findRichContentEditor().vm.$emit('input', newContent);
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('sets publish toolbar as saveable when content changes', () => {
+ expect(findPublishToolbar().props('saveable')).toBe(true);
+ });
+
+ it('sets publish toolbar as not saveable when content changes are rollback', () => {
+ findRichContentEditor().vm.$emit('input', content);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findPublishToolbar().props('saveable')).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
index 69646abade5..281b764a801 100644
--- a/spec/frontend/static_site_editor/pages/home_spec.js
+++ b/spec/frontend/static_site_editor/pages/home_spec.js
@@ -1,21 +1,20 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlSkeletonLoader } from '@gitlab/ui';
import createState from '~/static_site_editor/store/state';
import Home from '~/static_site_editor/pages/home.vue';
-import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
-import EditHeader from '~/static_site_editor/components/edit_header.vue';
+
+import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
+import EditArea from '~/static_site_editor/components/edit_area.vue';
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
-import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
import {
returnUrl,
- sourceContent,
- sourceContentTitle,
+ sourceContent as content,
+ sourceContentTitle as title,
savedContentMeta,
submitChangesError,
} from '../mock_data';
@@ -27,13 +26,12 @@ localVue.use(Vuex);
describe('static_site_editor/pages/home', () => {
let wrapper;
let store;
- let loadContentActionMock;
+ let $apollo;
let setContentActionMock;
let submitChangesActionMock;
let dismissSubmitChangesErrorActionMock;
const buildStore = ({ initialState, getters } = {}) => {
- loadContentActionMock = jest.fn();
setContentActionMock = jest.fn();
submitChangesActionMock = jest.fn();
dismissSubmitChangesErrorActionMock = jest.fn();
@@ -47,53 +45,55 @@ describe('static_site_editor/pages/home', () => {
...getters,
},
actions: {
- loadContent: loadContentActionMock,
setContent: setContentActionMock,
submitChanges: submitChangesActionMock,
dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
},
});
};
- const buildContentLoadedStore = ({ initialState, getters } = {}) => {
- buildStore({
- initialState: {
- isContentLoaded: true,
- ...initialState,
- },
- getters: {
- ...getters,
+
+ const buildApollo = (queries = {}) => {
+ $apollo = {
+ queries: {
+ sourceContent: {
+ loading: false,
+ },
+ ...queries,
},
- });
+ };
};
- const buildWrapper = (data = { appData: { isSupportedContent: true } }) => {
+ const buildWrapper = (data = {}) => {
wrapper = shallowMount(Home, {
localVue,
store,
- provide: {
- glFeatures: { richContentEditor: true },
+ mocks: {
+ $apollo,
},
data() {
- return data;
+ return {
+ appData: { isSupportedContent: true, returnUrl },
+ ...data,
+ };
},
});
};
- const findRichContentEditor = () => wrapper.find(RichContentEditor);
- const findEditHeader = () => wrapper.find(EditHeader);
+ const findEditArea = () => wrapper.find(EditArea);
const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
- const findPublishToolbar = () => wrapper.find(PublishToolbar);
- const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
+ const findSkeletonLoader = () => wrapper.find(SkeletonLoader);
const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage);
beforeEach(() => {
+ buildApollo();
buildStore();
- buildWrapper();
});
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
+ $apollo = null;
});
it('renders the saved changes message when changes are submitted successfully', () => {
@@ -107,103 +107,69 @@ describe('static_site_editor/pages/home', () => {
});
});
- describe('when content is not loaded', () => {
- it('does not render rich content editor', () => {
- expect(findRichContentEditor().exists()).toBe(false);
- });
-
- it('does not render edit header', () => {
- expect(findEditHeader().exists()).toBe(false);
- });
-
- it('does not render toolbar', () => {
- expect(findPublishToolbar().exists()).toBe(false);
- });
+ it('does not render the saved changes message when changes are not submitted', () => {
+ buildWrapper();
- it('does not render saved changes message', () => {
- expect(findSavedChangesMessage().exists()).toBe(false);
- });
+ expect(findSavedChangesMessage().exists()).toBe(false);
});
describe('when content is loaded', () => {
- const content = sourceContent;
- const title = sourceContentTitle;
-
beforeEach(() => {
- buildContentLoadedStore({ initialState: { content, title } });
- buildWrapper();
+ buildStore({ initialState: { isSavingChanges: true } });
+ buildWrapper({ sourceContent: { title, content } });
});
- it('renders the rich content editor', () => {
- expect(findRichContentEditor().exists()).toBe(true);
+ it('renders edit area', () => {
+ expect(findEditArea().exists()).toBe(true);
});
- it('renders the edit header', () => {
- expect(findEditHeader().exists()).toBe(true);
+ it('provides source content to the edit area', () => {
+ expect(findEditArea().props()).toMatchObject({
+ title,
+ content,
+ });
});
- it('does not render skeleton loader', () => {
- expect(findSkeletonLoader().exists()).toBe(false);
+ it('provides returnUrl to the edit area', () => {
+ expect(findEditArea().props('returnUrl')).toBe(returnUrl);
});
- it('passes page content to the rich content editor', () => {
- expect(findRichContentEditor().props('value')).toBe(content);
+ it('provides isSavingChanges to the edit area', () => {
+ expect(findEditArea().props('savingChanges')).toBe(true);
});
+ });
- it('passes page title to edit header', () => {
- expect(findEditHeader().props('title')).toBe(title);
- });
+ it('does not render edit area when content is not loaded', () => {
+ buildWrapper({ sourceContent: null });
- it('renders toolbar', () => {
- expect(findPublishToolbar().exists()).toBe(true);
- });
+ expect(findEditArea().exists()).toBe(false);
});
- it('sets toolbar as saveable when content changes', () => {
- buildContentLoadedStore({
- getters: {
- contentChanged: () => true,
+ it('renders skeleton loader when content is not loading', () => {
+ buildApollo({
+ sourceContent: {
+ loading: true,
},
});
buildWrapper();
- expect(findPublishToolbar().props('saveable')).toBe(true);
- });
-
- it('displays skeleton loader when loading content', () => {
- buildStore({ initialState: { isLoadingContent: true } });
- buildWrapper();
-
expect(findSkeletonLoader().exists()).toBe(true);
});
- it('does not display submit changes error when an error does not exist', () => {
- buildContentLoadedStore();
- buildWrapper();
-
- expect(findSubmitChangesError().exists()).toBe(false);
- });
-
- it('sets toolbar as saving when saving changes', () => {
- buildContentLoadedStore({
- initialState: {
- isSavingChanges: true,
+ it('does not render skeleton loader when content is not loading', () => {
+ buildApollo({
+ sourceContent: {
+ loading: false,
},
});
buildWrapper();
- expect(findPublishToolbar().props('savingChanges')).toBe(true);
- });
-
- it('displays invalid content message when content is not supported', () => {
- buildWrapper({ appData: { isSupportedContent: false } });
-
- expect(findInvalidContentMessage().exists()).toBe(true);
+ expect(findSkeletonLoader().exists()).toBe(false);
});
describe('when submitting changes fail', () => {
beforeEach(() => {
- buildContentLoadedStore({
+ buildStore({
initialState: {
submitChangesError,
},
@@ -228,24 +194,32 @@ describe('static_site_editor/pages/home', () => {
});
});
- it('dispatches load content action', () => {
- expect(loadContentActionMock).toHaveBeenCalled();
- });
-
- it('dispatches setContent action when rich content editor emits input event', () => {
- buildContentLoadedStore();
+ it('does not display submit changes error when an error does not exist', () => {
buildWrapper();
- findRichContentEditor().vm.$emit('input', sourceContent);
+ expect(findSubmitChangesError().exists()).toBe(false);
+ });
- expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined);
+ it('displays invalid content message when content is not supported', () => {
+ buildWrapper({ appData: { isSupportedContent: false } });
+
+ expect(findInvalidContentMessage().exists()).toBe(true);
});
- it('dispatches submitChanges action when toolbar emits submit event', () => {
- buildContentLoadedStore();
- buildWrapper();
- findPublishToolbar().vm.$emit('submit');
+ describe('when edit area emits submit event', () => {
+ const newContent = `new ${content}`;
- expect(submitChangesActionMock).toHaveBeenCalled();
+ beforeEach(() => {
+ buildWrapper({ sourceContent: { title, content } });
+ findEditArea().vm.$emit('submit', { content: newContent });
+ });
+
+ it('dispatches setContent property', () => {
+ expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), newContent, undefined);
+ });
+
+ it('dispatches submitChanges action', () => {
+ expect(submitChangesActionMock).toHaveBeenCalled();
+ });
});
});
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 951ee3c9b3d..74c769f86a3 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,10 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import { 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 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';
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
@@ -78,16 +79,6 @@ describe('DropdownContentsLabelsView', () => {
});
describe('methods', () => {
- describe('getDropdownLabelBoxStyle', () => {
- it('returns an object containing `backgroundColor` based on provided `label` param', () => {
- expect(wrapper.vm.getDropdownLabelBoxStyle(mockRegularLabel)).toEqual(
- expect.objectContaining({
- backgroundColor: mockRegularLabel.color,
- }),
- );
- });
- });
-
describe('isLabelSelected', () => {
it('returns true when provided `label` param is one of the selected labels', () => {
expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true);
@@ -234,16 +225,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders label elements for all labels', () => {
- const labelsEl = wrapper.findAll('.dropdown-content li');
- const labelItemEl = labelsEl.at(0).find(GlLink);
-
- expect(labelsEl.length).toBe(mockLabels.length);
- expect(labelItemEl.exists()).toBe(true);
- expect(labelItemEl.find(GlIcon).props('name')).toBe('mobile-issue-close');
- expect(labelItemEl.find('.dropdown-label-box').attributes('style')).toBe(
- 'background-color: rgb(186, 218, 85);',
- );
- expect(labelItemEl.find(GlLink).text()).toContain(mockLabels[0].title);
+ expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
});
it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
@@ -253,9 +235,9 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const labelsEl = wrapper.findAll('.dropdown-content li');
- const labelItemEl = labelsEl.at(0).find(GlLink);
+ const labelItemEl = labelsEl.at(0).find(LabelItem);
- expect(labelItemEl.attributes('class')).toContain('is-focused');
+ expect(labelItemEl.props('highlight')).toBe(true);
});
});
@@ -267,7 +249,7 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const noMatchEl = wrapper.find('.dropdown-content li');
- expect(noMatchEl.exists()).toBe(true);
+ expect(noMatchEl.isVisible()).toBe(true);
expect(noMatchEl.text()).toContain('No matching results');
});
});
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
new file mode 100644
index 00000000000..401d208da5c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
@@ -0,0 +1,111 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlIcon, GlLink } from '@gitlab/ui';
+import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
+import { mockRegularLabel } from './mock_data';
+
+const createComponent = ({ label = mockRegularLabel, highlight = true } = {}) =>
+ shallowMount(LabelItem, {
+ propsData: {
+ label,
+ highlight,
+ },
+ });
+
+describe('LabelItem', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('labelBoxStyle', () => {
+ it('returns an object containing `backgroundColor` based on `label` prop', () => {
+ expect(wrapper.vm.labelBoxStyle).toEqual(
+ expect.objectContaining({
+ backgroundColor: mockRegularLabel.color,
+ }),
+ );
+ });
+ });
+ });
+
+ 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([mockRegularLabel]);
+ });
+ });
+ });
+
+ 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({
+ highlight: true,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(GlLink).classes()).toContain('is-focused');
+ });
+ });
+
+ it('renders visible gl-icon component when `isSet` prop is true', () => {
+ wrapper.setData({
+ isSet: true,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const iconEl = wrapper.find(GlIcon);
+
+ expect(iconEl.isVisible()).toBe(true);
+ expect(iconEl.props('name')).toBe('mobile-issue-close');
+ });
+ });
+
+ it('renders visible span element as placeholder instead of gl-icon when `isSet` prop is false', () => {
+ wrapper.setData({
+ isSet: false,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const placeholderEl = wrapper.find('[data-testid="no-icon"]');
+
+ expect(placeholderEl.isVisible()).toBe(true);
+ });
+ });
+
+ it('renders label color element', () => {
+ const colorEl = wrapper.find('[data-testid="label-color-box"]');
+
+ expect(colorEl.exists()).toBe(true);
+ expect(colorEl.attributes('style')).toBe('background-color: rgb(186, 218, 85);');
+ });
+
+ it('renders label title', () => {
+ expect(wrapper.text()).toContain(mockRegularLabel.title);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index 3031df3362f..8081806e314 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -155,29 +155,12 @@ describe('LabelsSelect Mutations', () => {
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
- it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `true`', () => {
- const updatedLabelIds = [2, 4];
- const state = {
- labels,
- allowMultiselect: true,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
-
- state.labels.forEach(label => {
- if (updatedLabelIds.includes(label.id)) {
- expect(label.touched).toBe(true);
- expect(label.set).toBe(true);
- }
- });
- });
-
- it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `false`', () => {
+ it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
const updatedLabelIds = [2];
const state = {
labels,
- allowMultiselect: false,
};
- mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
state.labels.forEach(label => {
if (updatedLabelIds.includes(label.id)) {
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 3574066e03e..4ce7143bdf0 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -85,4 +85,19 @@ describe MilestonesHelper do
end
end
end
+
+ describe "#group_milestone_route" do
+ let(:group) { build_stubbed(:group) }
+ let(:subgroup) { build_stubbed(:group, parent: group, name: "Test Subgrp") }
+
+ context "when in subgroup" do
+ let(:milestone) { build_stubbed(:group_milestone, group: subgroup) }
+
+ it 'generates correct url despite assigned @group' do
+ assign(:group, group)
+ milestone_path = "/groups/#{subgroup.full_path}/-/milestones/#{milestone.iid}"
+ expect(helper.group_milestone_route(milestone)).to eq(milestone_path)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
index c3bb975727b..34ac70071bb 100644
--- a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeReques
expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3)
- migration.perform(1, mr.id)
+ migration.backfill_range(1, mr.id)
expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero
expect(deployment_merge_requests.count).to eq(2)
diff --git a/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb
new file mode 100644
index 00000000000..21ee42295cd
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::Summary::Value do
+ describe Gitlab::CycleAnalytics::Summary::Value::None do
+ it 'returns `-`' do
+ expect(described_class.new.to_s).to eq('-')
+ end
+ end
+
+ describe Gitlab::CycleAnalytics::Summary::Value::Numeric do
+ it 'returns the string representation of the number' do
+ expect(described_class.new(3.2).to_s).to eq('3.2')
+ end
+ end
+
+ describe Gitlab::CycleAnalytics::Summary::Value::PrettyNumeric do
+ describe '#to_s' do
+ it 'returns `-` when the number is 0' do
+ expect(described_class.new(0).to_s).to eq('-')
+ end
+
+ it 'returns the string representation of the number' do
+ expect(described_class.new(100).to_s).to eq('100')
+ end
+ end
+ end
+end
diff --git a/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb b/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb
deleted file mode 100644
index 296ae07cc21..00000000000
--- a/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200312134637_backfill_environment_id_on_deployment_merge_requests.rb')
-
-describe BackfillEnvironmentIdOnDeploymentMergeRequests do
- let(:environments) { table(:environments) }
- let(:merge_requests) { table(:merge_requests) }
- let(:deployments) { table(:deployments) }
- let(:deployment_merge_requests) { table(:deployment_merge_requests) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
-
- let(:migration_worker) { double('BackgroundMigrationWorker') }
-
- before do
- stub_const('BackgroundMigrationWorker', migration_worker)
- end
-
- it 'schedules nothing when there are no entries' do
- expect(migration_worker).not_to receive(:perform_in)
-
- migrate!
- end
-
- it 'batches the workload' do
- stub_const("#{described_class.name}::BATCH_SIZE", 10)
-
- namespace = namespaces.create!(name: 'foo', path: 'foo')
- project = projects.create!(namespace_id: namespace.id)
-
- environment = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
-
- # Batching is based on DeploymentMergeRequest.merge_request_id, in order to test it
- # we must generate more than described_class::BATCH_SIZE merge requests, deployments,
- # and deployment_merge_requests entries
- entries = 13
- expect(entries).to be > described_class::BATCH_SIZE
-
- # merge requests and deployments bulk generation
- mrs_params = []
- deployments_params = []
- entries.times do |i|
- mrs_params << { source_branch: 'x', target_branch: 'master', target_project_id: project.id }
-
- deployments_params << { environment_id: environment.id, iid: i + 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1 }
- end
-
- all_mrs = merge_requests.insert_all(mrs_params)
- all_deployments = deployments.insert_all(deployments_params)
-
- # deployment_merge_requests bulk generation
- dmr_params = []
- entries.times do |index|
- mr_id = all_mrs.rows[index].first
- deployment_id = all_deployments.rows[index].first
-
- dmr_params << { deployment_id: deployment_id, merge_request_id: mr_id }
- end
-
- deployment_merge_requests.insert_all(dmr_params)
-
- first_batch_limit = dmr_params[described_class::BATCH_SIZE][:merge_request_id]
- second_batch_limit = dmr_params.last[:merge_request_id]
-
- expect(migration_worker).to receive(:perform_in)
- .with(
- 0,
- 'BackfillEnvironmentIdDeploymentMergeRequests',
- [1, first_batch_limit]
- )
- expect(migration_worker).to receive(:perform_in)
- .with(
- described_class::DELAY,
- 'BackfillEnvironmentIdDeploymentMergeRequests',
- [first_batch_limit + 1, second_batch_limit]
- )
-
- migrate!
- end
-end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 5fe0a9052cf..38b92f22faf 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -3836,40 +3836,28 @@ describe MergeRequest do
end
describe '#diffable_merge_ref?' do
- context 'diff_compare_with_head enabled' do
- context 'merge request can be merged' do
- context 'merge_to_ref is not calculated' do
- it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(false)
- end
- end
-
- context 'merge_to_ref is calculated' do
- before do
- MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
- end
-
- it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(true)
- end
+ context 'merge request can be merged' do
+ context 'merge_to_ref is not calculated' do
+ it 'returns true' do
+ expect(subject.diffable_merge_ref?).to eq(false)
end
end
- context 'merge request cannot be merged' do
- it 'returns false' do
- subject.mark_as_unchecked!
+ context 'merge_to_ref is calculated' do
+ before do
+ MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
+ end
- expect(subject.diffable_merge_ref?).to eq(false)
+ it 'returns true' do
+ expect(subject.diffable_merge_ref?).to eq(true)
end
end
end
- context 'diff_compare_with_head disabled' do
- before do
- stub_feature_flags(diff_compare_with_head: { enabled: false, thing: subject.target_project })
- end
-
+ context 'merge request cannot be merged' do
it 'returns false' do
+ subject.mark_as_unchecked!
+
expect(subject.diffable_merge_ref?).to eq(false)
end
end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index fedaae372c4..087bc957373 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -326,4 +326,26 @@ describe SentNotification do
end
end
end
+
+ describe "#position=" do
+ subject { build(:sent_notification, noteable: create(:issue)) }
+
+ it "doesn't accept non-hash JSON passed as a string" do
+ subject.position = "true"
+
+ expect(subject.attributes_before_type_cast["position"]).to be(nil)
+ end
+
+ it "does accept a position hash as a string" do
+ subject.position = '{ "base_sha": "test" }'
+
+ expect(subject.position.base_sha).to eq("test")
+ end
+
+ it "does accept a hash" do
+ subject.position = { "base_sha" => "test" }
+
+ expect(subject.position.base_sha).to eq("test")
+ end
+ end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 333f4e560cf..f29ed26f2aa 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -176,15 +176,21 @@ describe Ci::BuildPolicy do
end
context 'when developers can push to the branch' do
- before do
- create(:protected_branch, :developers_can_push,
- name: build.ref, project: project)
- end
-
context 'when the build was created by the developer' do
let(:owner) { user }
- it { expect(policy).to be_allowed :erase_build }
+ context 'when the build was created for a protected ref' do
+ before do
+ create(:protected_branch, :developers_can_push,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+
+ context 'when the build was created for an unprotected ref' do
+ it { expect(policy).to be_allowed :erase_build }
+ end
end
context 'when the build was created by the other' do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 5e8223ec3cc..f2dc5b1c045 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -630,7 +630,7 @@ describe API::Branches do
post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to eq('Invalid reference name: new_design3')
+ expect(json_response['message']).to eq('Invalid reference name: foo')
end
end
diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb
index 406ab8959a2..8340f5a7db2 100644
--- a/spec/requests/api/graphql/mutations/branches/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb
@@ -38,7 +38,7 @@ describe 'Creation of a new branch' do
let(:ref) { 'unknown' }
it_behaves_like 'a mutation that returns errors in the response',
- errors: ['Invalid reference name: new_branch']
+ errors: ['Invalid reference name: unknown']
end
end
end
diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
index ce20d494542..3cb1dbbbc2c 100644
--- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
+++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
@@ -159,6 +159,17 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do
SOURCE
end
+ it 'does not flag the double use of `X_if_ee` on the last line' do
+ expect_no_offenses(<<~SOURCE)
+ class Foo
+ end
+
+ Foo.extend_if_ee('EE::Foo')
+ Foo.include_if_ee('EE::Foo')
+ Foo.prepend_if_ee('EE::Foo')
+ SOURCE
+ end
+
it 'autocorrects offenses by just disabling the Cop' do
source = <<~SOURCE
class Foo
diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb
index b0629c5e25a..072a86d17fc 100644
--- a/spec/services/branches/create_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -3,39 +3,45 @@
require 'spec_helper'
describe Branches::CreateService do
- let(:user) { create(:user) }
-
subject(:service) { described_class.new(project, user) }
+ let_it_be(:project) { create(:project_empty_repo) }
+ let_it_be(:user) { create(:user) }
+
describe '#execute' do
context 'when repository is empty' do
- let(:project) { create(:project_empty_repo) }
-
it 'creates master branch' do
service.execute('my-feature', 'master')
expect(project.repository.branch_exists?('master')).to be_truthy
end
- it 'creates my-feature branch' do
- service.execute('my-feature', 'master')
+ it 'creates another-feature branch' do
+ service.execute('another-feature', 'master')
- expect(project.repository.branch_exists?('my-feature')).to be_truthy
+ expect(project.repository.branch_exists?('another-feature')).to be_truthy
end
end
- context 'when creating a branch fails' do
- let(:project) { create(:project_empty_repo) }
+ context 'when branch already exists' do
+ it 'returns an error' do
+ result = service.execute('master', 'master')
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Branch already exists')
+ end
+ end
+ context 'when incorrect reference is provided' do
before do
allow(project.repository).to receive(:add_branch).and_return(false)
end
- it 'returns an error with the branch name' do
- result = service.execute('my-feature', 'master')
+ it 'returns an error with a reference name' do
+ result = service.execute('new-feature', 'unknown')
expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq("Invalid reference name: my-feature")
+ expect(result[:message]).to eq('Invalid reference name: unknown')
end
end
end
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index 259148fa18f..259148fa18f 100755..100644
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index a1c2a238a96..a1c2a238a96 100755..100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore