summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue19
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue12
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue62
-rw-r--r--app/assets/javascripts/logs/components/log_simple_filters.vue42
-rw-r--r--app/assets/javascripts/milestones/stores/actions.js58
-rw-r--r--app/assets/javascripts/milestones/stores/getters.js2
-rw-r--r--app/assets/javascripts/milestones/stores/index.js16
-rw-r--r--app/assets/javascripts/milestones/stores/mutation_types.js13
-rw-r--r--app/assets/javascripts/milestones/stores/mutations.js44
-rw-r--r--app/assets/javascripts/milestones/stores/state.js14
-rw-r--r--app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/editor_lite.vue4
-rw-r--r--app/models/container_expiration_policy.rb11
-rw-r--r--app/services/merge_requests/cleanup_refs_service.rb2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/workers/container_expiration_policy_worker.rb16
-rw-r--r--changelogs/unreleased/229023-migrate-sidebar-epic-tooltip.yml5
-rw-r--r--changelogs/unreleased/229330-update-discard-changes-button.yml5
-rw-r--r--changelogs/unreleased/263110-improve-cleanup-policies-selection-during-their-execution.yml5
-rw-r--r--changelogs/unreleased/270054-fix-no-method-error.yml5
-rw-r--r--changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-lo.yml5
-rw-r--r--changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-pa.yml5
-rw-r--r--changelogs/unreleased/add-vuex-store-for-milestone-combobox.yml5
-rw-r--r--db/migrate/20201009090954_add_index_with_project_id_to_container_expiration_policies.rb19
-rw-r--r--db/schema_migrations/202010090909541
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/pages/index.md8
-rw-r--r--doc/development/img/architecture_simplified.pngbin21239 -> 106339 bytes
-rw-r--r--doc/install/README.md60
-rw-r--r--doc/user/application_security/coverage_fuzzing/index.md46
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json2
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js16
-rw-r--r--spec/frontend/logs/components/log_simple_filters_spec.js8
-rw-r--r--spec/frontend/milestones/stores/actions_spec.js140
-rw-r--r--spec/frontend/milestones/stores/getter_spec.js15
-rw-r--r--spec/frontend/milestones/stores/mutations_spec.js159
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap91
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/editor_lite_spec.js35
-rw-r--r--spec/models/container_expiration_policy_spec.rb12
-rw-r--r--spec/services/merge_requests/cleanup_refs_service_spec.rb11
-rw-r--r--spec/workers/container_expiration_policy_worker_spec.rb46
-rw-r--r--yarn.lock8
44 files changed, 835 insertions, 254 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
index 279fcfe736f..b16d960402b 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
@@ -4,7 +4,11 @@ import { GlAlert } from '@gitlab/ui';
import { mapKeys, mapValues, pick, some, sum } from 'lodash';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { s__ } from '~/locale';
-import { formatDateAsMonth, getDayDifference } from '~/lib/utils/datetime_utility';
+import {
+ differenceInMonths,
+ formatDateAsMonth,
+ getDayDifference,
+} from '~/lib/utils/datetime_utility';
import { getAverageByMonth, sortByDate, extractValues } from '../utils';
import pipelineStatsQuery from '../graphql/queries/pipeline_stats.query.graphql';
import { TODAY, START_DATE } from '../constants';
@@ -150,19 +154,14 @@ export default {
max: this.$options.endDate,
};
},
- differenceInMonths() {
- const yearDiff = this.$options.endDate.getYear() - this.$options.startDate.getYear();
- const monthDiff = this.$options.endDate.getMonth() - this.$options.startDate.getMonth();
-
- return monthDiff + 12 * yearDiff;
- },
chartOptions() {
+ const { endDate, startDate, i18n } = this.$options;
return {
xAxis: {
...this.range,
- name: this.$options.i18n.xAxisTitle,
+ name: i18n.xAxisTitle,
type: 'time',
- splitNumber: this.differenceInMonths + 1,
+ splitNumber: differenceInMonths(startDate, endDate) + 1,
axisLabel: {
interval: 0,
showMinLabel: false,
@@ -172,7 +171,7 @@ export default {
},
},
yAxis: {
- name: this.$options.i18n.yAxisTitle,
+ name: i18n.yAxisTitle,
},
};
},
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index bbcb866c758..53fac09ab66 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlButton } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
@@ -8,6 +8,7 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
components: {
GlModal,
+ GlButton,
FileIcon,
ChangedFileIcon,
},
@@ -52,15 +53,16 @@ export default {
</strong>
<changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto">
- <button
+ <gl-button
v-if="canDiscard"
ref="discardButton"
- type="button"
- class="btn btn-remove btn-inverted gl-mr-3"
+ category="secondary"
+ variant="danger"
+ class="gl-mr-3"
@click="showDiscardModal"
>
{{ __('Discard changes') }}
- </button>
+ </gl-button>
</div>
<gl-modal
ref="discardModal"
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 97b96cb5839..f7c0bd5ae13 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -3,12 +3,11 @@ import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlSprintf,
- GlIcon,
GlAlert,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownItem,
- GlDeprecatedDropdownDivider,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlDropdownDivider,
GlInfiniteScroll,
} from '@gitlab/ui';
@@ -23,12 +22,11 @@ import { formatDate } from '../utils';
export default {
components: {
GlSprintf,
- GlIcon,
GlAlert,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownItem,
- GlDeprecatedDropdownDivider,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlDropdownDivider,
GlInfiniteScroll,
LogSimpleFilters,
LogAdvancedFilters,
@@ -174,46 +172,38 @@ export default {
<div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2">
<div class="flex-grow-0">
- <gl-deprecated-dropdown
+ <gl-dropdown
id="environments-dropdown"
:text="environments.current || managedApps.current"
:disabled="environments.isLoading"
- class="mb-2 gl-h-32 pr-2 d-flex d-md-block js-environments-dropdown"
+ class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block js-environments-dropdown"
>
- <gl-deprecated-dropdown-header class="gl-text-center">
+ <gl-dropdown-section-header>
{{ s__('Environments|Environments') }}
- </gl-deprecated-dropdown-header>
- <gl-deprecated-dropdown-item
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
v-for="env in environments.options"
:key="env.id"
+ :is-check-item="true"
+ :is-checked="isCurrentEnvironment(env.name)"
@click="showEnvironment(env.name)"
>
- <div class="d-flex">
- <gl-icon
- :class="{ invisible: !isCurrentEnvironment(env.name) }"
- name="status_success_borderless"
- />
- <div class="gl-flex-grow-1">{{ env.name }}</div>
- </div>
- </gl-deprecated-dropdown-item>
- <gl-deprecated-dropdown-divider />
- <gl-deprecated-dropdown-header class="gl-text-center">
+ {{ env.name }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-section-header>
{{ s__('Environments|Managed apps') }}
- </gl-deprecated-dropdown-header>
- <gl-deprecated-dropdown-item
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
v-for="app in managedApps.options"
:key="app.id"
+ :is-check-item="true"
+ :is-checked="isCurrentManagedApp(app.name)"
@click="showManagedApp(app.name)"
>
- <div class="gl-display-flex">
- <gl-icon
- :class="{ invisible: !isCurrentManagedApp(app.name) }"
- name="status_success_borderless"
- />
- <div class="gl-flex-grow-1">{{ app.name }}</div>
- </div>
- </gl-deprecated-dropdown-item>
- </gl-deprecated-dropdown>
+ {{ app.name }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
<log-advanced-filters
diff --git a/app/assets/javascripts/logs/components/log_simple_filters.vue b/app/assets/javascripts/logs/components/log_simple_filters.vue
index 2e1270b5428..ba30d4628c9 100644
--- a/app/assets/javascripts/logs/components/log_simple_filters.vue
+++ b/app/assets/javascripts/logs/components/log_simple_filters.vue
@@ -1,19 +1,13 @@
<script>
import { mapActions, mapState } from 'vuex';
-import {
- GlIcon,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownItem,
-} from '@gitlab/ui';
+import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
- GlIcon,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownItem,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
},
props: {
disabled: {
@@ -44,35 +38,31 @@ export default {
</script>
<template>
<div>
- <gl-deprecated-dropdown
+ <gl-dropdown
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
- class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown"
+ class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block qa-pods-dropdown"
>
- <gl-deprecated-dropdown-header class="text-center">
+ <gl-dropdown-section-header>
{{ s__('Environments|Select pod') }}
- </gl-deprecated-dropdown-header>
+ </gl-dropdown-section-header>
- <gl-deprecated-dropdown-item v-if="!pods.options.length" disabled>
+ <gl-dropdown-item v-if="!pods.options.length" disabled>
<span ref="noPodsMsg" class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
- </gl-deprecated-dropdown-item>
- <gl-deprecated-dropdown-item
+ </gl-dropdown-item>
+ <gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
+ :is-check-item="true"
+ :is-checked="isCurrentPod(podName)"
class="text-nowrap"
@click="showPodLogs(podName)"
>
- <div class="d-flex">
- <gl-icon
- :class="{ invisible: !isCurrentPod(podName) }"
- name="status_success_borderless"
- />
- <div class="flex-grow-1">{{ podName }}</div>
- </div>
- </gl-deprecated-dropdown-item>
- </gl-deprecated-dropdown>
+ {{ podName }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/milestones/stores/actions.js b/app/assets/javascripts/milestones/stores/actions.js
new file mode 100644
index 00000000000..3859771aeba
--- /dev/null
+++ b/app/assets/javascripts/milestones/stores/actions.js
@@ -0,0 +1,58 @@
+import Api from '~/api';
+import * as types from './mutation_types';
+
+export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
+
+export const setSelectedMilestones = ({ commit }, selectedMilestones) =>
+ commit(types.SET_SELECTED_MILESTONES, selectedMilestones);
+
+export const toggleMilestones = ({ commit, state }, selectedMilestone) => {
+ const removeMilestone = state.selectedMilestones.includes(selectedMilestone);
+
+ if (removeMilestone) {
+ commit(types.REMOVE_SELECTED_MILESTONE, selectedMilestone);
+ } else {
+ commit(types.ADD_SELECTED_MILESTONE, selectedMilestone);
+ }
+};
+
+export const search = ({ dispatch, commit }, query) => {
+ commit(types.SET_QUERY, query);
+
+ dispatch('searchMilestones');
+};
+
+export const fetchMilestones = ({ commit, state }) => {
+ commit(types.REQUEST_START);
+
+ Api.projectMilestones(state.projectId)
+ .then(response => {
+ commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response);
+ })
+ .catch(error => {
+ commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error);
+ })
+ .finally(() => {
+ commit(types.REQUEST_FINISH);
+ });
+};
+
+export const searchMilestones = ({ commit, state }) => {
+ commit(types.REQUEST_START);
+
+ const options = {
+ search: state.query,
+ scope: 'milestones',
+ };
+
+ Api.projectSearch(state.projectId, options)
+ .then(response => {
+ commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response);
+ })
+ .catch(error => {
+ commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error);
+ })
+ .finally(() => {
+ commit(types.REQUEST_FINISH);
+ });
+};
diff --git a/app/assets/javascripts/milestones/stores/getters.js b/app/assets/javascripts/milestones/stores/getters.js
new file mode 100644
index 00000000000..d8a283403ec
--- /dev/null
+++ b/app/assets/javascripts/milestones/stores/getters.js
@@ -0,0 +1,2 @@
+/** Returns `true` if there is at least one in-progress request */
+export const isLoading = ({ requestCount }) => requestCount > 0;
diff --git a/app/assets/javascripts/milestones/stores/index.js b/app/assets/javascripts/milestones/stores/index.js
new file mode 100644
index 00000000000..2bebffc19ab
--- /dev/null
+++ b/app/assets/javascripts/milestones/stores/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import createState from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: createState(),
+ });
diff --git a/app/assets/javascripts/milestones/stores/mutation_types.js b/app/assets/javascripts/milestones/stores/mutation_types.js
new file mode 100644
index 00000000000..370d386dba2
--- /dev/null
+++ b/app/assets/javascripts/milestones/stores/mutation_types.js
@@ -0,0 +1,13 @@
+export const SET_PROJECT_ID = 'SET_PROJECT_ID';
+
+export const SET_SELECTED_MILESTONES = 'SET_SELECTED_MILESTONES';
+export const ADD_SELECTED_MILESTONE = 'ADD_SELECTED_MILESTONE';
+export const REMOVE_SELECTED_MILESTONE = 'REMOVE_SELECTED_MILESTONE';
+
+export const SET_QUERY = 'SET_QUERY';
+
+export const REQUEST_START = 'REQUEST_START';
+export const REQUEST_FINISH = 'REQUEST_FINISH';
+
+export const RECEIVE_PROJECT_MILESTONES_SUCCESS = 'RECEIVE_PROJECT_MILESTONES_SUCCESS';
+export const RECEIVE_PROJECT_MILESTONES_ERROR = 'RECEIVE_PROJECT_MILESTONES_ERROR';
diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js
new file mode 100644
index 00000000000..7c75d09766c
--- /dev/null
+++ b/app/assets/javascripts/milestones/stores/mutations.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import * as types from './mutation_types';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export default {
+ [types.SET_PROJECT_ID](state, projectId) {
+ state.projectId = projectId;
+ },
+ [types.SET_SELECTED_MILESTONES](state, selectedMilestones) {
+ Vue.set(state, 'selectedMilestones', selectedMilestones);
+ },
+ [types.ADD_SELECTED_MILESTONE](state, selectedMilestone) {
+ state.selectedMilestones.push(selectedMilestone);
+ },
+ [types.REMOVE_SELECTED_MILESTONE](state, selectedMilestone) {
+ const filteredMilestones = state.selectedMilestones.filter(
+ milestone => milestone !== selectedMilestone,
+ );
+ Vue.set(state, 'selectedMilestones', filteredMilestones);
+ },
+ [types.SET_QUERY](state, query) {
+ state.query = query;
+ },
+ [types.REQUEST_START](state) {
+ state.requestCount += 1;
+ },
+ [types.REQUEST_FINISH](state) {
+ state.requestCount -= 1;
+ },
+ [types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
+ state.matches.projectMilestones = {
+ list: convertObjectPropsToCamelCase(response.data).map(({ title }) => ({ title })),
+ totalCount: parseInt(response.headers['x-total'], 10),
+ error: null,
+ };
+ },
+ [types.RECEIVE_PROJECT_MILESTONES_ERROR](state, error) {
+ state.matches.projectMilestones = {
+ list: [],
+ totalCount: 0,
+ error,
+ };
+ },
+};
diff --git a/app/assets/javascripts/milestones/stores/state.js b/app/assets/javascripts/milestones/stores/state.js
new file mode 100644
index 00000000000..0944539f367
--- /dev/null
+++ b/app/assets/javascripts/milestones/stores/state.js
@@ -0,0 +1,14 @@
+export default () => ({
+ projectId: null,
+ groupId: null,
+ query: '',
+ matches: {
+ projectMilestones: {
+ list: [],
+ totalCount: 0,
+ error: null,
+ },
+ },
+ selectedMilestones: [],
+ requestCount: 0,
+});
diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
index 5d59880d497..a9079f91f50 100644
--- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
+++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlAlert,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
- GlIcon,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { get } from 'lodash';
@@ -17,9 +11,8 @@ export default {
components: {
GlAlert,
GlAreaChart,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
- GlIcon,
+ GlDropdown,
+ GlDropdownItem,
GlSprintf,
},
props: {
@@ -140,25 +133,18 @@ export default {
{{ __('It seems that there is currently no available data for code coverage') }}
</span>
</gl-alert>
- <gl-deprecated-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
- <gl-deprecated-dropdown-item
+ <gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
+ <gl-dropdown-item
v-for="({ group_name }, index) in dailyCoverageData"
:key="index"
:value="group_name"
+ :is-check-item="true"
+ :is-checked="index === selectedCoverageIndex"
@click="setSelectedCoverage(index)"
>
- <div class="gl-display-flex">
- <gl-icon
- v-if="index === selectedCoverageIndex"
- name="mobile-issue-close"
- class="gl-absolute"
- />
- <span class="gl-display-flex align-items-center ml-4">
- {{ group_name }}
- </span>
- </div>
- </gl-deprecated-dropdown-item>
- </gl-deprecated-dropdown>
+ {{ group_name }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
<gl-area-chart
v-if="!isLoading"
diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue
index bc3a9ee45f8..cfe3ce0a11c 100644
--- a/app/assets/javascripts/vue_shared/components/editor_lite.vue
+++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue
@@ -58,7 +58,9 @@ export default {
this.editor.updateModelLanguage(newVal);
},
value(newVal) {
- this.editor.setValue(newVal);
+ if (this.editor.getValue() !== newVal) {
+ this.editor.setValue(newVal);
+ }
},
},
mounted() {
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index b1dd720d908..641d244b665 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -3,6 +3,7 @@
class ContainerExpirationPolicy < ApplicationRecord
include Schedulable
include UsageStatistics
+ include EachBatch
belongs_to :project, inverse_of: :container_expiration_policy
@@ -19,6 +20,16 @@ class ContainerExpirationPolicy < ApplicationRecord
scope :active, -> { where(enabled: true) }
scope :preloaded, -> { preload(project: [:route]) }
+ def self.executable
+ runnable_schedules.where(
+ 'EXISTS (?)',
+ ContainerRepository.select(1)
+ .where(
+ 'container_repositories.project_id = container_expiration_policies.project_id'
+ )
+ )
+ end
+
def self.keep_n_options
{
1 => _('%{tags} tag per image name') % { tags: 1 },
diff --git a/app/services/merge_requests/cleanup_refs_service.rb b/app/services/merge_requests/cleanup_refs_service.rb
index 0f03f5f09b4..d003124a112 100644
--- a/app/services/merge_requests/cleanup_refs_service.rb
+++ b/app/services/merge_requests/cleanup_refs_service.rb
@@ -17,7 +17,7 @@ module MergeRequests
@repository = merge_request.project.repository
@ref_path = merge_request.ref_path
@merge_ref_path = merge_request.merge_ref_path
- @ref_head_sha = @repository.commit(merge_request.ref_path).id
+ @ref_head_sha = @repository.commit(merge_request.ref_path)&.id
@merge_ref_sha = merge_request.merge_ref_head&.id
end
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index c5d7b148e69..f6dc808aa55 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -92,7 +92,7 @@
%li.nav-item
%div
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
- = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'gl-button btn btn-sign-in'
+ = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only= _('Toggle navigation')
diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb
index 96590e165ae..61ba27f00d2 100644
--- a/app/workers/container_expiration_policy_worker.rb
+++ b/app/workers/container_expiration_policy_worker.rb
@@ -7,13 +7,15 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
feature_category :container_registry
def perform
- ContainerExpirationPolicy.runnable_schedules.preloaded.find_each do |container_expiration_policy|
- with_context(project: container_expiration_policy.project,
- user: container_expiration_policy.project.owner) do |project:, user:|
- ContainerExpirationPolicyService.new(project, user)
- .execute(container_expiration_policy)
- rescue ContainerExpirationPolicyService::InvalidPolicyError => e
- Gitlab::ErrorTracking.log_exception(e, container_expiration_policy_id: container_expiration_policy.id)
+ ContainerExpirationPolicy.executable.preloaded.each_batch do |relation|
+ relation.each do |container_expiration_policy|
+ with_context(project: container_expiration_policy.project,
+ user: container_expiration_policy.project.owner) do |project:, user:|
+ ContainerExpirationPolicyService.new(project, user)
+ .execute(container_expiration_policy)
+ rescue ContainerExpirationPolicyService::InvalidPolicyError => e
+ Gitlab::ErrorTracking.log_exception(e, container_expiration_policy_id: container_expiration_policy.id)
+ end
end
end
end
diff --git a/changelogs/unreleased/229023-migrate-sidebar-epic-tooltip.yml b/changelogs/unreleased/229023-migrate-sidebar-epic-tooltip.yml
new file mode 100644
index 00000000000..cf13a49aa84
--- /dev/null
+++ b/changelogs/unreleased/229023-migrate-sidebar-epic-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Replace tooltip with GLTooltip in epic sidebar datepicker
+merge_request: 45392
+author:
+type: other
diff --git a/changelogs/unreleased/229330-update-discard-changes-button.yml b/changelogs/unreleased/229330-update-discard-changes-button.yml
new file mode 100644
index 00000000000..58e60d4a12c
--- /dev/null
+++ b/changelogs/unreleased/229330-update-discard-changes-button.yml
@@ -0,0 +1,5 @@
+---
+title: Updated Discard Changes button in WebIDE
+merge_request: 41899
+author:
+type: changed
diff --git a/changelogs/unreleased/263110-improve-cleanup-policies-selection-during-their-execution.yml b/changelogs/unreleased/263110-improve-cleanup-policies-selection-during-their-execution.yml
new file mode 100644
index 00000000000..bff321cd5b9
--- /dev/null
+++ b/changelogs/unreleased/263110-improve-cleanup-policies-selection-during-their-execution.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude policies with no container repositories when executing them
+merge_request: 44748
+author:
+type: fixed
diff --git a/changelogs/unreleased/270054-fix-no-method-error.yml b/changelogs/unreleased/270054-fix-no-method-error.yml
new file mode 100644
index 00000000000..ae3706062f2
--- /dev/null
+++ b/changelogs/unreleased/270054-fix-no-method-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error when cleaning up MR with no head ref
+merge_request: 45504
+author:
+type: fixed
diff --git a/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-lo.yml b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-lo.yml
new file mode 100644
index 00000000000..ccac03b006f
--- /dev/null
+++ b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-lo.yml
@@ -0,0 +1,5 @@
+---
+title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/logs
+merge_request: 41421
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-pa.yml b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-pa.yml
new file mode 100644
index 00000000000..8638d578605
--- /dev/null
+++ b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-pa.yml
@@ -0,0 +1,5 @@
+---
+title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
+merge_request: 41423
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/add-vuex-store-for-milestone-combobox.yml b/changelogs/unreleased/add-vuex-store-for-milestone-combobox.yml
new file mode 100644
index 00000000000..88610c6ec29
--- /dev/null
+++ b/changelogs/unreleased/add-vuex-store-for-milestone-combobox.yml
@@ -0,0 +1,5 @@
+---
+title: Add vuex stores for milestone comboxbox
+merge_request: 45287
+author:
+type: added
diff --git a/db/migrate/20201009090954_add_index_with_project_id_to_container_expiration_policies.rb b/db/migrate/20201009090954_add_index_with_project_id_to_container_expiration_policies.rb
new file mode 100644
index 00000000000..ec44d5ddcef
--- /dev/null
+++ b/db/migrate/20201009090954_add_index_with_project_id_to_container_expiration_policies.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexWithProjectIdToContainerExpirationPolicies < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ INDEX_NAME = 'idx_container_exp_policies_on_project_id_next_run_at_enabled'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :container_expiration_policies, [:project_id, :next_run_at, :enabled], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :container_expiration_policies, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20201009090954 b/db/schema_migrations/20201009090954
new file mode 100644
index 00000000000..5d5ca8ff29b
--- /dev/null
+++ b/db/schema_migrations/20201009090954
@@ -0,0 +1 @@
+d0944a864a1a89e9339eb1f8ffab683df1a5bb90f7b7a16cabd4871f34d1cd48 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c8b3b8fb587..4e6fc7e9260 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19622,6 +19622,8 @@ CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_ev
CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1);
+CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at_enabled ON container_expiration_policies USING btree (project_id, next_run_at, enabled);
+
CREATE INDEX idx_deployment_clusters_on_cluster_id_and_kubernetes_namespace ON deployment_clusters USING btree (cluster_id, kubernetes_namespace);
CREATE UNIQUE INDEX idx_deployment_merge_requests_unique_index ON deployment_merge_requests USING btree (deployment_id, merge_request_id);
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 61d9261650a..9f72293a730 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -597,7 +597,7 @@ database encryption. Proceed with caution.
```ruby
pages_external_url "http://<pages_server_URL>"
gitlab_pages['enable'] = false
- gitlab_rails['pages_enabled']=false
+ pages_nginx['enable'] = false
gitlab_rails['pages_path'] = "/mnt/pages"
```
@@ -788,3 +788,9 @@ This problem most likely results from an [out-dated operating system](https://do
The [Pages daemon uses the `securecookie` library](https://gitlab.com/search?group_id=9970&project_id=734943&repository_ref=master&scope=blobs&search=securecookie&snippets=false) to get random strings via [crypto/rand in Go](https://golang.org/pkg/crypto/rand/#pkg-variables).
This requires the `getrandom` syscall or `/dev/urandom` to be available on the host OS.
Upgrading to an [officially supported operating system](https://about.gitlab.com/install/) is recommended.
+
+### The requested scope is invalid, malformed, or unknown
+
+This problem comes from the permissions of the GitLab Pages OAuth application. To fix it, go to
+**Admin > Applications > GitLab Pages** and edit the application. Under **Scopes**, ensure that the
+`api` scope is selected and save your changes.
diff --git a/doc/development/img/architecture_simplified.png b/doc/development/img/architecture_simplified.png
index 46ae2b3c055..72d00b91129 100644
--- a/doc/development/img/architecture_simplified.png
+++ b/doc/development/img/architecture_simplified.png
Binary files differ
diff --git a/doc/install/README.md b/doc/install/README.md
index 518b94d1694..6b08bb28bbb 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -9,33 +9,42 @@ type: index
# Installation **(CORE ONLY)**
-GitLab can be installed in most GNU/Linux distributions and in a number
-of cloud providers. To get the best experience from GitLab, you need to balance
-performance, reliability, ease of administration (backups, upgrades and troubleshooting),
-and cost of hosting.
-
-There are many ways you can install GitLab depending on your platform:
-
-1. [**Omnibus GitLab**](#installing-gitlab-using-the-omnibus-gitlab-package-recommended): The official deb/rpm packages that contain a bundle of GitLab
- and the various components it depends on, like PostgreSQL, Redis, Sidekiq, etc.
-1. [**GitLab Helm chart**](#installing-gitlab-on-kubernetes-via-the-gitlab-helm-charts): The cloud native Helm chart for installing GitLab and all its components on Kubernetes.
-1. [**Docker**](#installing-gitlab-with-docker): The Omnibus GitLab packages dockerized.
-1. [**Source**](#installing-gitlab-from-source): Install GitLab and all its components from scratch.
-1. [**Cloud provider**](#installing-gitlab-on-cloud-providers): Install directly from platforms like AWS, Azure, GCP.
-
-TIP: **If in doubt, choose Omnibus:**
-The Omnibus GitLab packages are mature,
-[scalable](../administration/reference_architectures/index.md) and are used
+GitLab can be installed in most GNU/Linux distributions and with several
+cloud providers. To get the best experience from GitLab, you must balance
+performance, reliability, ease of administration (backups, upgrades, and
+troubleshooting), and the cost of hosting.
+
+Depending on your platform, select from the following available methods to
+install GitLab:
+
+- [_Omnibus GitLab_](#installing-gitlab-using-the-omnibus-gitlab-package-recommended):
+ The official deb/rpm packages that contain a bundle of GitLab and the
+ components it depends on, including PostgreSQL, Redis, and Sidekiq.
+- [_GitLab Helm chart_](#installing-gitlab-on-kubernetes-via-the-gitlab-helm-charts):
+ The cloud native Helm chart for installing GitLab and all of its components
+ on Kubernetes.
+- [_Docker_](#installing-gitlab-with-docker): The Omnibus GitLab packages,
+ dockerized.
+- [_Source_](#installing-gitlab-from-source): Install GitLab and all of its
+ components from scratch.
+- [_Cloud provider_](#installing-gitlab-on-cloud-providers): Install directly
+ from platforms like AWS, Azure, and GCP.
+
+If you're not sure which installation method to use, we recommend you use
+Omnibus GitLab. The Omnibus GitLab packages are mature,
+[scalable](../administration/reference_architectures/index.md), and are used
today on GitLab.com. The Helm charts are recommended for those who are familiar
with Kubernetes.
## Requirements
-Before installing GitLab, it is of critical importance to review the system [requirements](requirements.md). The system requirements include details on the minimum hardware, software, database, and additional requirements to support GitLab.
+Before you install GitLab, be sure to review the [system requirements](requirements.md).
+The system requirements include details about the minimum hardware, software,
+database, and additional requirements to support GitLab.
## Installing GitLab using the Omnibus GitLab package (recommended)
-The Omnibus GitLab package uses our official deb/rpm repositories. This is
+The Omnibus GitLab package uses our official deb/rpm repositories, and is
recommended for most users.
If you need additional flexibility and resilience, we recommend deploying
@@ -45,11 +54,6 @@ GitLab as described in our [reference architecture documentation](../administrat
## Installing GitLab on Kubernetes via the GitLab Helm charts
-NOTE: **Kubernetes experience required:**
-We recommend being familiar with Kubernetes before using it to deploy GitLab in
-production. The methods for management, observability, and some concepts are
-different than traditional deployments.
-
When installing GitLab on Kubernetes, there are some trade-offs that you
need to be aware of:
@@ -59,11 +63,17 @@ need to be aware of:
are deployed in a redundant fashion.
- There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations).
+Due to these trade-offs, having Kubernetes experience is a requirement for
+using this method. We recommend being familiar with Kubernetes before using it
+to deploy GitLab in production. The methods for management, observability, and
+some concepts are different than traditional deployments.
+
[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](https://docs.gitlab.com/charts/)
## Installing GitLab with Docker
-GitLab maintains a set of official Docker images based on the Omnibus GitLab package.
+GitLab maintains a set of official Docker images based on the Omnibus GitLab
+package.
[**> Install GitLab using the official GitLab Docker images.**](docker.md)
diff --git a/doc/user/application_security/coverage_fuzzing/index.md b/doc/user/application_security/coverage_fuzzing/index.md
index dff71cb9445..9508407ccae 100644
--- a/doc/user/application_security/coverage_fuzzing/index.md
+++ b/doc/user/application_security/coverage_fuzzing/index.md
@@ -175,6 +175,52 @@ To use coverage fuzzing in an offline environment, follow these steps:
`NEW_URL_GITLAB_COV_FUZ` is the URL of the private `gitlab-cov-fuzz` clone that you set up in the
first step.
+### Continuous fuzzing (long-running async fuzzing jobs)
+
+It's also possible to run the fuzzing jobs longer and without blocking your main pipeline. This
+configuration uses the GitLab [parent-child pipelines](../../../ci/parent_child_pipelines.md).
+The full example is available in the [repository](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example/-/tree/continuous_fuzzing#running-go-fuzz-from-ci).
+This example uses Go, but is applicable for any other supported languages.
+
+The suggested workflow in this scenario is to have long-running, async fuzzing jobs on a
+main/development branch, and short, blocking sync fuzzing jobs on all other branches and MRs. This
+is a good way to balance the needs of letting a developer's per-commit pipeline complete quickly,
+and also giving the fuzzer a large amount of time to fully explore and test the app.
+
+Long-running fuzzing jobs are usually necessary for the coverage guided fuzzer to find deeper bugs
+in your latest code base. THe following is an example of what `.gitlab-ci.yml` looks like in this
+workflow (for the full example, see the [repository](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example/-/tree/continuous_fuzzing)):
+
+```yaml
+
+sync_fuzzing:
+ variables:
+ COVFUZZ_ADDITIONAL_ARGS: '-max_total_time=300'
+ trigger:
+ include: .covfuzz-ci.yml
+ strategy: depend
+ rules:
+ - if: $CI_COMMIT_BRANCH != 'continuous_fuzzing' && $CI_PIPELINE_SOURCE != 'merge_request_event'
+
+async_fuzzing:
+ variables:
+ COVFUZZ_ADDITIONAL_ARGS: '-max_total_time=3600'
+ trigger:
+ include: .covfuzz-ci.yml
+ rules:
+ - if: $CI_COMMIT_BRANCH == 'continuous_fuzzing' && $CI_PIPELINE_SOURCE != 'merge_request_event'
+```
+
+This essentially creates two steps:
+
+1. `sync_fuzzing`: Runs all your fuzz targets for a short period of time in a blocking
+ configuration. This finds simple bugs and allows you to be confident that your MRs aren't
+ introducing new bugs or causing old bugs to reappear.
+1. `async_fuzzing`: Runs on your branch and finds deep bugs in your code without blocking your
+ development cycle and MRs.
+
+The `covfuzz-ci.yml` is the same as that in the [original synchronous example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example#running-go-fuzz-from-ci).
+
### Glossary
- Seed corpus: The set of test cases given as initial input to the fuzz target. This usually speeds
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index def2098d7e8..53dc8ed68e1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18490,9 +18490,6 @@ msgstr ""
msgid "Open issues"
msgstr ""
-msgid "Open projects"
-msgstr ""
-
msgid "Open raw"
msgstr ""
@@ -22103,6 +22100,9 @@ msgstr ""
msgid "Replication"
msgstr ""
+msgid "Replication details"
+msgstr ""
+
msgid "Replication enabled"
msgstr ""
diff --git a/package.json b/package.json
index 1511c095d82..a182da72b28 100644
--- a/package.json
+++ b/package.json
@@ -147,7 +147,7 @@
"vue": "^2.6.12",
"vue-apollo": "^3.0.3",
"vue-loader": "^15.9.3",
- "vue-router": "^3.4.6",
+ "vue-router": "^3.4.7",
"vue-template-compiler": "^2.6.12",
"vue-virtual-scroll-list": "^1.4.4",
"vuedraggable": "^2.23.0",
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index 559ce4f9414..e32deaea993 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -1,4 +1,4 @@
-import { GlSprintf, GlIcon, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlSprintf, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EnvironmentLogs from '~/logs/components/environment_logs.vue';
@@ -121,7 +121,7 @@ describe('EnvironmentLogs', () => {
it('displays UI elements', () => {
initWrapper();
- expect(findEnvironmentsDropdown().is(GlDeprecatedDropdown)).toBe(true);
+ expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
expect(findSimpleFilters().exists()).toBe(true);
expect(findLogControlButtons().exists()).toBe(true);
@@ -164,7 +164,7 @@ describe('EnvironmentLogs', () => {
it('displays a disabled environments dropdown', () => {
expect(findEnvironmentsDropdown().attributes('disabled')).toBe('true');
- expect(findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem).length).toBe(0);
+ expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0);
});
it('does not update buttons state', () => {
@@ -241,7 +241,7 @@ describe('EnvironmentLogs', () => {
});
it('populates environments dropdown', () => {
- const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem);
+ const items = findEnvironmentsDropdown().findAll(GlDropdownItem);
expect(findEnvironmentsDropdown().props('text')).toBe(mockEnvName);
expect(items.length).toBe(mockEnvironments.length);
mockEnvironments.forEach((env, i) => {
@@ -251,14 +251,14 @@ describe('EnvironmentLogs', () => {
});
it('dropdown has one environment selected', () => {
- const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem);
+ const items = findEnvironmentsDropdown().findAll(GlDropdownItem);
mockEnvironments.forEach((env, i) => {
const item = items.at(i);
if (item.text() !== mockEnvName) {
- expect(item.find(GlIcon).classes('invisible')).toBe(true);
+ expect(item.find(GlDropdownItem).attributes('ischecked')).toBeFalsy();
} else {
- expect(item.find(GlIcon).classes('invisible')).toBe(false);
+ expect(item.find(GlDropdownItem).attributes('ischecked')).toBeTruthy();
}
});
});
@@ -286,7 +286,7 @@ describe('EnvironmentLogs', () => {
describe('when user clicks', () => {
it('environment name, trace is refreshed', () => {
- const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem);
+ const items = findEnvironmentsDropdown().findAll(GlDropdownItem);
const index = 1; // any env
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showEnvironment`, expect.anything());
diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js
index 1e30a7df559..b819f0d25a8 100644
--- a/spec/frontend/logs/components/log_simple_filters_spec.js
+++ b/spec/frontend/logs/components/log_simple_filters_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/logs/stores';
import { mockPods, mockPodName } from '../mock_data';
@@ -17,7 +17,7 @@ describe('LogSimpleFilters', () => {
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
const findPodsDropdownItems = () =>
findPodsDropdown()
- .findAll(GlDeprecatedDropdownItem)
+ .findAll(GlDropdownItem)
.filter(item => !('disabled' in item.attributes()));
const mockPodsLoading = () => {
@@ -114,9 +114,9 @@ describe('LogSimpleFilters', () => {
mockPods.forEach((pod, i) => {
const item = items.at(i);
if (item.text() !== mockPodName) {
- expect(item.find(GlIcon).classes('invisible')).toBe(true);
+ expect(item.find(GlDropdownItem).attributes('ischecked')).toBeFalsy();
} else {
- expect(item.find(GlIcon).classes('invisible')).toBe(false);
+ expect(item.find(GlDropdownItem).attributes('ischecked')).toBeTruthy();
}
});
});
diff --git a/spec/frontend/milestones/stores/actions_spec.js b/spec/frontend/milestones/stores/actions_spec.js
new file mode 100644
index 00000000000..ad73d0e4238
--- /dev/null
+++ b/spec/frontend/milestones/stores/actions_spec.js
@@ -0,0 +1,140 @@
+import testAction from 'helpers/vuex_action_helper';
+import createState from '~/milestones/stores/state';
+import * as actions from '~/milestones/stores/actions';
+import * as types from '~/milestones/stores/mutation_types';
+
+let mockProjectMilestonesReturnValue;
+let mockProjectSearchReturnValue;
+
+jest.mock('~/api', () => ({
+ // `__esModule: true` is required when mocking modules with default exports:
+ // https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options
+ __esModule: true,
+ default: {
+ projectMilestones: () => mockProjectMilestonesReturnValue,
+ projectSearch: () => mockProjectSearchReturnValue,
+ },
+}));
+
+describe('Milestone combobox Vuex store actions', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe('setProjectId', () => {
+ it(`commits ${types.SET_PROJECT_ID} with the new project ID`, () => {
+ const projectId = '4';
+ testAction(actions.setProjectId, projectId, state, [
+ { type: types.SET_PROJECT_ID, payload: projectId },
+ ]);
+ });
+ });
+
+ describe('setSelectedMilestones', () => {
+ it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => {
+ const selectedMilestones = ['v1.2.3'];
+ testAction(actions.setSelectedMilestones, selectedMilestones, state, [
+ { type: types.SET_SELECTED_MILESTONES, payload: selectedMilestones },
+ ]);
+ });
+ });
+
+ describe('toggleMilestones', () => {
+ const selectedMilestone = 'v1.2.3';
+ it(`commits ${types.ADD_SELECTED_MILESTONE} with the new selected milestone name`, () => {
+ testAction(actions.toggleMilestones, selectedMilestone, state, [
+ { type: types.ADD_SELECTED_MILESTONE, payload: selectedMilestone },
+ ]);
+ });
+
+ it(`commits ${types.REMOVE_SELECTED_MILESTONE} with the new selected milestone name`, () => {
+ state.selectedMilestones = [selectedMilestone];
+ testAction(actions.toggleMilestones, selectedMilestone, state, [
+ { type: types.REMOVE_SELECTED_MILESTONE, payload: selectedMilestone },
+ ]);
+ });
+ });
+
+ describe('search', () => {
+ it(`commits ${types.SET_QUERY} with the new search query`, () => {
+ const query = 'v1.0';
+ testAction(
+ actions.search,
+ query,
+ state,
+ [{ type: types.SET_QUERY, payload: query }],
+ [{ type: 'searchMilestones' }],
+ );
+ });
+ });
+
+ describe('searchMilestones', () => {
+ describe('when the search is successful', () => {
+ const projectSearchApiResponse = { data: [{ title: 'v1.0' }] };
+
+ beforeEach(() => {
+ mockProjectSearchReturnValue = Promise.resolve(projectSearchApiResponse);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.searchMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+
+ describe('when the search fails', () => {
+ const error = new Error('Something went wrong!');
+
+ beforeEach(() => {
+ mockProjectSearchReturnValue = Promise.reject(error);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.searchMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+ });
+
+ describe('fetchMilestones', () => {
+ describe('when the fetch is successful', () => {
+ const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] };
+
+ beforeEach(() => {
+ mockProjectMilestonesReturnValue = Promise.resolve(projectMilestonesApiResponse);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.fetchMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+
+ describe('when the fetch fails', () => {
+ const error = new Error('Something went wrong!');
+
+ beforeEach(() => {
+ mockProjectMilestonesReturnValue = Promise.reject(error);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.fetchMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/milestones/stores/getter_spec.js b/spec/frontend/milestones/stores/getter_spec.js
new file mode 100644
index 00000000000..df7c3d28e67
--- /dev/null
+++ b/spec/frontend/milestones/stores/getter_spec.js
@@ -0,0 +1,15 @@
+import * as getters from '~/milestones/stores/getters';
+
+describe('Milestone comboxbox Vuex store getters', () => {
+ describe('isLoading', () => {
+ it.each`
+ requestCount | isLoading
+ ${2} | ${true}
+ ${1} | ${true}
+ ${0} | ${false}
+ ${-1} | ${false}
+ `('returns true when at least one request is in progress', ({ requestCount, isLoading }) => {
+ expect(getters.isLoading({ requestCount })).toBe(isLoading);
+ });
+ });
+});
diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js
new file mode 100644
index 00000000000..8f8ce3c87ad
--- /dev/null
+++ b/spec/frontend/milestones/stores/mutations_spec.js
@@ -0,0 +1,159 @@
+import createState from '~/milestones/stores/state';
+import mutations from '~/milestones/stores/mutations';
+import * as types from '~/milestones/stores/mutation_types';
+
+describe('Milestones combobox Vuex store mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe('initial state', () => {
+ it('is created with the correct structure and initial values', () => {
+ expect(state).toEqual({
+ projectId: null,
+ groupId: null,
+ query: '',
+ matches: {
+ projectMilestones: {
+ list: [],
+ totalCount: 0,
+ error: null,
+ },
+ },
+ selectedMilestones: [],
+ requestCount: 0,
+ });
+ });
+ });
+
+ describe(`${types.SET_PROJECT_ID}`, () => {
+ it('updates the project ID', () => {
+ const newProjectId = '4';
+ mutations[types.SET_PROJECT_ID](state, newProjectId);
+
+ expect(state.projectId).toBe(newProjectId);
+ });
+ });
+
+ describe(`${types.SET_SELECTED_MILESTONES}`, () => {
+ it('sets the selected milestones', () => {
+ const selectedMilestones = ['v1.2.3'];
+ mutations[types.SET_SELECTED_MILESTONES](state, selectedMilestones);
+
+ expect(state.selectedMilestones).toEqual(['v1.2.3']);
+ });
+ });
+
+ describe(`${types.ADD_SELECTED_MILESTONESs}`, () => {
+ it('adds the selected milestones', () => {
+ const selectedMilestone = 'v1.2.3';
+ mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone);
+
+ expect(state.selectedMilestones).toEqual(['v1.2.3']);
+ });
+ });
+
+ describe(`${types.REMOVE_SELECTED_MILESTONES}`, () => {
+ it('removes the selected milestones', () => {
+ const selectedMilestone = 'v1.2.3';
+
+ mutations[types.SET_SELECTED_MILESTONES](state, [selectedMilestone]);
+ expect(state.selectedMilestones).toEqual(['v1.2.3']);
+
+ mutations[types.REMOVE_SELECTED_MILESTONE](state, selectedMilestone);
+ expect(state.selectedMilestones).toEqual([]);
+ });
+ });
+
+ describe(`${types.SET_QUERY}`, () => {
+ it('updates the search query', () => {
+ const newQuery = 'hello';
+ mutations[types.SET_QUERY](state, newQuery);
+
+ expect(state.query).toBe(newQuery);
+ });
+ });
+
+ describe(`${types.REQUEST_START}`, () => {
+ it('increments requestCount by 1', () => {
+ mutations[types.REQUEST_START](state);
+ expect(state.requestCount).toBe(1);
+
+ mutations[types.REQUEST_START](state);
+ expect(state.requestCount).toBe(2);
+
+ mutations[types.REQUEST_START](state);
+ expect(state.requestCount).toBe(3);
+ });
+ });
+
+ describe(`${types.REQUEST_FINISH}`, () => {
+ it('decrements requestCount by 1', () => {
+ state.requestCount = 3;
+
+ mutations[types.REQUEST_FINISH](state);
+ expect(state.requestCount).toBe(2);
+
+ mutations[types.REQUEST_FINISH](state);
+ expect(state.requestCount).toBe(1);
+
+ mutations[types.REQUEST_FINISH](state);
+ expect(state.requestCount).toBe(0);
+ });
+ });
+
+ describe(`${types.RECEIVE_PROJECT_MILESTONES_SUCCESS}`, () => {
+ it('updates state.matches.projectMilestones based on the provided API response', () => {
+ const response = {
+ data: [
+ {
+ title: 'v0.1',
+ },
+ {
+ title: 'v0.2',
+ },
+ ],
+ headers: {
+ 'x-total': 2,
+ },
+ };
+
+ mutations[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response);
+
+ expect(state.matches.projectMilestones).toEqual({
+ list: [
+ {
+ title: 'v0.1',
+ },
+ {
+ title: 'v0.2',
+ },
+ ],
+ error: null,
+ totalCount: 2,
+ });
+ });
+
+ describe(`${types.RECEIVE_PROJECT_MILESTONES_ERROR}`, () => {
+ it('updates state.matches.projectMilestones to an empty state with the error object', () => {
+ const error = new Error('Something went wrong!');
+
+ state.matches.projectMilestones = {
+ list: [{ title: 'v0.1' }],
+ totalCount: 1,
+ error: null,
+ };
+
+ mutations[types.RECEIVE_PROJECT_MILESTONES_ERROR](state, error);
+
+ expect(state.matches.projectMilestones).toEqual({
+ list: [],
+ totalCount: 0,
+ error,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index 211f4ea20f5..8ccad7d5c22 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -9,65 +9,54 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<!---->
- <gl-deprecated-dropdown-stub
+ <gl-dropdown-stub
+ category="tertiary"
+ headertext=""
+ size="medium"
text="rspec"
+ variant="default"
>
- <gl-deprecated-dropdown-item-stub
+ <gl-dropdown-item-stub
+ avatarurl=""
+ iconcolor=""
+ iconname=""
+ iconrightname=""
+ ischecked="true"
+ ischeckitem="true"
+ secondarytext=""
value="rspec"
>
- <div
- class="gl-display-flex"
- >
- <gl-icon-stub
- class="gl-absolute"
- name="mobile-issue-close"
- size="16"
- />
-
- <span
- class="gl-display-flex align-items-center ml-4"
- >
-
- rspec
-
- </span>
- </div>
- </gl-deprecated-dropdown-item-stub>
- <gl-deprecated-dropdown-item-stub
+
+ rspec
+
+ </gl-dropdown-item-stub>
+ <gl-dropdown-item-stub
+ avatarurl=""
+ iconcolor=""
+ iconname=""
+ iconrightname=""
+ ischeckitem="true"
+ secondarytext=""
value="cypress"
>
- <div
- class="gl-display-flex"
- >
- <!---->
-
- <span
- class="gl-display-flex align-items-center ml-4"
- >
-
- cypress
-
- </span>
- </div>
- </gl-deprecated-dropdown-item-stub>
- <gl-deprecated-dropdown-item-stub
+
+ cypress
+
+ </gl-dropdown-item-stub>
+ <gl-dropdown-item-stub
+ avatarurl=""
+ iconcolor=""
+ iconname=""
+ iconrightname=""
+ ischeckitem="true"
+ secondarytext=""
value="karma"
>
- <div
- class="gl-display-flex"
- >
- <!---->
-
- <span
- class="gl-display-flex align-items-center ml-4"
- >
-
- karma
-
- </span>
- </div>
- </gl-deprecated-dropdown-item-stub>
- </gl-deprecated-dropdown-stub>
+
+ karma
+
+ </gl-dropdown-item-stub>
+ </gl-dropdown-stub>
</div>
<gl-area-chart-stub
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 8884f7815ab..4a60c7fd509 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
-import { GlAlert, GlIcon, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import waitForPromises from 'helpers/wait_for_promises';
@@ -17,7 +17,7 @@ describe('Code Coverage', () => {
const findAlert = () => wrapper.find(GlAlert);
const findAreaChart = () => wrapper.find(GlAreaChart);
- const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem);
+ const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownItem = () => findAllDropdownItems().at(0);
const findSecondDropdownItem = () => findAllDropdownItems().at(1);
@@ -124,7 +124,7 @@ describe('Code Coverage', () => {
});
it('renders the dropdown with all custom names as options', () => {
- expect(wrapper.find(GlDeprecatedDropdown).exists()).toBeDefined();
+ expect(wrapper.find(GlDropdown).exists()).toBeDefined();
expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
});
@@ -145,16 +145,8 @@ describe('Code Coverage', () => {
await wrapper.vm.$nextTick();
- expect(
- findFirstDropdownItem()
- .find(GlIcon)
- .exists(),
- ).toBe(false);
- expect(
- findSecondDropdownItem()
- .find(GlIcon)
- .exists(),
- ).toBe(true);
+ expect(findFirstDropdownItem().attributes('ischecked')).toBeFalsy();
+ expect(findSecondDropdownItem().attributes('ischecked')).toBeTruthy();
});
it('updates the graph data when selecting a different option in dropdown', async () => {
diff --git a/spec/frontend/vue_shared/components/editor_lite_spec.js b/spec/frontend/vue_shared/components/editor_lite_spec.js
index 48005484b91..52502fcf64f 100644
--- a/spec/frontend/vue_shared/components/editor_lite_spec.js
+++ b/spec/frontend/vue_shared/components/editor_lite_spec.js
@@ -96,17 +96,6 @@ describe('Editor Lite component', () => {
});
});
- it('reacts to the changes in the pased value', async () => {
- const newValue = 'New Value';
-
- wrapper.setProps({
- value: newValue,
- });
-
- await nextTick();
- expect(setValue).toHaveBeenCalledWith(newValue);
- });
-
it('registers callback with editor onChangeContent', () => {
expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
});
@@ -127,5 +116,29 @@ describe('Editor Lite component', () => {
expect(wrapper.emitted()['editor-ready']).toBeDefined();
});
+
+ describe('reaction to the value update', () => {
+ it('reacts to the changes in the passed value', async () => {
+ const newValue = 'New Value';
+
+ wrapper.setProps({
+ value: newValue,
+ });
+
+ await nextTick();
+ expect(setValue).toHaveBeenCalledWith(newValue);
+ });
+
+ it("does not update value if the passed one is exactly the same as the editor's content", async () => {
+ const newValue = `${value}`; // to make sure we're creating a new String with the same content and not just a reference
+
+ wrapper.setProps({
+ value: newValue,
+ });
+
+ await nextTick();
+ expect(setValue).not.toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/spec/models/container_expiration_policy_spec.rb b/spec/models/container_expiration_policy_spec.rb
index 588685b04bf..1d9dbe8a867 100644
--- a/spec/models/container_expiration_policy_spec.rb
+++ b/spec/models/container_expiration_policy_spec.rb
@@ -104,6 +104,18 @@ RSpec.describe ContainerExpirationPolicy, type: :model do
end
end
+ describe '.executable' do
+ subject { described_class.executable }
+
+ let_it_be(:policy1) { create(:container_expiration_policy, :runnable) }
+ let_it_be(:container_repository1) { create(:container_repository, project: policy1.project) }
+ let_it_be(:policy2) { create(:container_expiration_policy, :runnable) }
+ let_it_be(:container_repository2) { create(:container_repository, project: policy2.project) }
+ let_it_be(:policy3) { create(:container_expiration_policy, :runnable) }
+
+ it { is_expected.to contain_exactly(policy1, policy2) }
+ end
+
describe '#disable!' do
let_it_be(:container_expiration_policy) { create(:container_expiration_policy) }
diff --git a/spec/services/merge_requests/cleanup_refs_service_spec.rb b/spec/services/merge_requests/cleanup_refs_service_spec.rb
index b38ccee4aa0..a051b3c9355 100644
--- a/spec/services/merge_requests/cleanup_refs_service_spec.rb
+++ b/spec/services/merge_requests/cleanup_refs_service_spec.rb
@@ -35,6 +35,17 @@ RSpec.describe MergeRequests::CleanupRefsService do
end
end
+ context 'when merge request has no head ref' do
+ before do
+ # Simulate a merge request with no head ref
+ merge_request.project.repository.delete_refs(merge_request.ref_path)
+ end
+
+ it 'does not fail' do
+ expect(result[:status]).to eq(:success)
+ end
+ end
+
context 'when merge request has merge ref' do
before do
MergeRequests::MergeToRefService
diff --git a/spec/workers/container_expiration_policy_worker_spec.rb b/spec/workers/container_expiration_policy_worker_spec.rb
index 868eb6b192e..6b185c30670 100644
--- a/spec/workers/container_expiration_policy_worker_spec.rb
+++ b/spec/workers/container_expiration_policy_worker_spec.rb
@@ -7,19 +7,24 @@ RSpec.describe ContainerExpirationPolicyWorker do
subject { described_class.new.perform }
- context 'With no container expiration policies' do
- it 'Does not execute any policies' do
+ RSpec.shared_examples 'not executing any policy' do
+ it 'does not run any policy' do
expect(ContainerExpirationPolicyService).not_to receive(:new)
subject
end
end
+ context 'With no container expiration policies' do
+ it_behaves_like 'not executing any policy'
+ end
+
context 'With container expiration policies' do
- context 'a valid policy' do
- let!(:container_expiration_policy) { create(:container_expiration_policy, :runnable) }
- let(:user) { container_expiration_policy.project.owner }
+ let_it_be(:container_expiration_policy, reload: true) { create(:container_expiration_policy, :runnable) }
+ let_it_be(:container_repository) { create(:container_repository, project: container_expiration_policy.project) }
+ let_it_be(:user) { container_expiration_policy.project.owner }
+ context 'a valid policy' do
it 'runs the policy' do
service = instance_double(ContainerExpirationPolicyService, execute: true)
@@ -31,33 +36,30 @@ RSpec.describe ContainerExpirationPolicyWorker do
end
context 'a disabled policy' do
- let!(:container_expiration_policy) { create(:container_expiration_policy, :runnable, :disabled) }
- let(:user) {container_expiration_policy.project.owner }
-
- it 'does not run the policy' do
- expect(ContainerExpirationPolicyService)
- .not_to receive(:new).with(container_expiration_policy, user)
-
- subject
+ before do
+ container_expiration_policy.disable!
end
+
+ it_behaves_like 'not executing any policy'
end
context 'a policy that is not due for a run' do
- let!(:container_expiration_policy) { create(:container_expiration_policy) }
- let(:user) {container_expiration_policy.project.owner }
+ before do
+ container_expiration_policy.update_column(:next_run_at, 2.minutes.from_now)
+ end
- it 'does not run the policy' do
- expect(ContainerExpirationPolicyService)
- .not_to receive(:new).with(container_expiration_policy, user)
+ it_behaves_like 'not executing any policy'
+ end
- subject
+ context 'a policy linked to no container repository' do
+ before do
+ container_expiration_policy.container_repositories.delete_all
end
+
+ it_behaves_like 'not executing any policy'
end
context 'an invalid policy' do
- let_it_be(:container_expiration_policy) { create(:container_expiration_policy, :runnable) }
- let_it_be(:user) {container_expiration_policy.project.owner }
-
before do
container_expiration_policy.update_column(:name_regex, '*production')
end
diff --git a/yarn.lock b/yarn.lock
index 8c4777f2797..83a9264bfb8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12368,10 +12368,10 @@ vue-loader@^15.9.3:
vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0"
-vue-router@^3.4.6:
- version "3.4.6"
- resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.6.tgz#f7bda2c9a43d39837621c9a02ba7789f5daa24b2"
- integrity sha512-kaXnB3pfFxhAJl/Mp+XG1HJMyFqrL/xPqV7oXlpXn4AwMmm6VNgf0nllW8ksflmZANfI4kdo0bVn/FYSsAolPQ==
+vue-router@^3.4.7:
+ version "3.4.7"
+ resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.7.tgz#bf189bafd16f4e4ef783c4a6250a3090f2c1fa1b"
+ integrity sha512-CbHXue5BLrDivOk5O4eZ0WT4Yj8XwdXa4kCnsEIOzYUPF/07ZukayA2jGxDCJxLc9SgVQX9QX0OuGOwGlVB4Qg==
vue-runtime-helpers@^1.1.2:
version "1.1.2"