diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-09 15:09:29 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-09 15:09:29 +0000 |
commit | 209bd8cf1f542f6ba2a069b368a9187faa871e96 (patch) | |
tree | 6b77dc8183135b8316cc70c8dbc9c4e7c18cf05a | |
parent | a9ced7da447785c57477b3d8dbccc73a78cface1 (diff) | |
download | gitlab-ce-209bd8cf1f542f6ba2a069b368a9187faa871e96.tar.gz |
Add latest changes from gitlab-org/gitlab@master
85 files changed, 1054 insertions, 454 deletions
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION index 7ec1d6db408..ccbccc3dc62 100644 --- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION +++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION @@ -1 +1 @@ -2.1.0 +2.2.0 diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 3c00ae98e75..cc9bfa2e174 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -326,6 +326,7 @@ export default { }, [types.SET_SHOW_WHITESPACE](state, showWhitespace) { state.showWhitespace = showWhitespace; + state.diffFiles = []; }, [types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) { state.fileFinderVisible = visible; diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index fdd27e08793..1678991b1ea 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -5,6 +5,7 @@ import { highCountTrim } from '~/lib/utils/text_utility'; import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue'; import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue'; import { parseBoolean } from '~/lib/utils/common_utils'; +import Tracking from '~/tracking'; /** * Updates todo counter when todos are toggled. @@ -73,6 +74,24 @@ function initStatusTriggers() { } } +export function initNavUserDropdownTracking() { + const el = document.querySelector('.js-nav-user-dropdown'); + const buyEl = document.querySelector('.js-buy-ci-minutes-link'); + + if (el && buyEl) { + const { trackLabel, trackProperty } = buyEl.dataset; + const trackEvent = 'show_buy_ci_minutes'; + + $(el).on('shown.bs.dropdown', () => { + Tracking.event(undefined, trackEvent, { + label: trackLabel, + property: trackProperty, + }); + }); + } +} + document.addEventListener('DOMContentLoaded', () => { requestIdleCallback(initStatusTriggers); + initNavUserDropdownTracking(); }); diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js index 87b4b14f6bf..94a0d38f05f 100644 --- a/app/assets/javascripts/helpers/monitor_helper.js +++ b/app/assets/javascripts/helpers/monitor_helper.js @@ -1,4 +1,64 @@ /** + * @param {String} queryLabel - Default query label for chart + * @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance) + * @returns {String} The formatted query label + * @example + * singleAttributeLabel('app', {__name__: "up", app: "prometheus"}) -> "app: prometheus" + */ +const singleAttributeLabel = (queryLabel, metricAttributes) => { + if (!queryLabel) return ''; + const relevantAttribute = queryLabel.toLowerCase().replace(' ', '_'); + const value = metricAttributes[relevantAttribute]; + if (!value) return ''; + return `${queryLabel}: ${value}`; +}; + +/** + * @param {String} queryLabel - Default query label for chart + * @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance) + * @returns {String} The formatted query label + * @example + * templatedLabel('__name__', {__name__: "up", app: "prometheus"}) -> "__name__" + */ +const templatedLabel = (queryLabel, metricAttributes) => { + if (!queryLabel) return ''; + // eslint-disable-next-line array-callback-return + Object.entries(metricAttributes).map(([templateVar, label]) => { + const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g'); + // eslint-disable-next-line no-param-reassign + queryLabel = queryLabel.replace(regex, label); + }); + + return queryLabel; +}; + +/** + * @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance) + * @returns {String} The formatted query label + * @example + * multiMetricLabel('', {__name__: "up", app: "prometheus"}) -> "__name__: up, app: prometheus" + */ +const multiMetricLabel = metricAttributes => { + return Object.entries(metricAttributes) + .map(([templateVar, label]) => `${templateVar}: ${label}`) + .join(', '); +}; + +/** + * @param {String} queryLabel - Default query label for chart + * @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance) + * @returns {String} The formatted query label + */ +const getSeriesLabel = (queryLabel, metricAttributes) => { + return ( + singleAttributeLabel(queryLabel, metricAttributes) || + templatedLabel(queryLabel, metricAttributes) || + multiMetricLabel(metricAttributes) || + queryLabel + ); +}; + +/** * @param {Array} queryResults - Array of Result objects * @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name) * @returns {Array} The formatted values @@ -12,21 +72,11 @@ export const makeDataSeries = (queryResults, defaultConfig) => if (!data.length) { return null; } - const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_'); - const name = result.metric[relevantMetric]; const series = { data }; - if (name) { - series.name = `${defaultConfig.name}: ${name}`; - } else { - series.name = defaultConfig.name; - Object.keys(result.metric).forEach(templateVar => { - const value = result.metric[templateVar]; - const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g'); - - series.name = series.name.replace(regex, value); - }); - } - - return { ...defaultConfig, ...series }; + return { + ...defaultConfig, + ...series, + name: getSeriesLabel(defaultConfig.name, result.metric), + }; }) .filter(series => series !== null); diff --git a/app/assets/javascripts/monitoring/components/charts/bar.vue b/app/assets/javascripts/monitoring/components/charts/bar.vue index 01fd8940dad..e1018cd5952 100644 --- a/app/assets/javascripts/monitoring/components/charts/bar.vue +++ b/app/assets/javascripts/monitoring/components/charts/bar.vue @@ -58,7 +58,7 @@ export default { }, methods: { formatLegendLabel(query) { - return `${query.label}`; + return query.label; }, onResize() { if (!this.$refs.barChart) return; diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue index 0ed801e6e57..7a2e3e1b511 100644 --- a/app/assets/javascripts/monitoring/components/charts/column.vue +++ b/app/assets/javascripts/monitoring/components/charts/column.vue @@ -76,7 +76,7 @@ export default { }, methods: { formatLegendLabel(query) { - return `${query.label}`; + return query.label; }, onResize() { if (!this.$refs.columnChart) return; diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index e43a0131528..f4cd6bbbb34 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -251,7 +251,7 @@ export default { }, methods: { formatLegendLabel(query) { - return `${query.label}`; + return query.label; }, isTooltipOfType(tooltipType, defaultType) { return tooltipType === defaultType; diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 5e620d6c2f5..d01acdd031b 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -68,12 +68,11 @@ export const parseEnvironmentsResponse = (response = [], projectPath) => * https://gitlab.com/gitlab-org/gitlab/issues/207198 * * @param {Array} metrics - Array of prometheus metrics - * @param {String} defaultLabel - Default label for metrics * @returns {Object} */ -const mapToMetricsViewModel = (metrics, defaultLabel) => +const mapToMetricsViewModel = metrics => metrics.map(({ label, id, metric_id, query_range, prometheus_endpoint_path, ...metric }) => ({ - label: label || defaultLabel, + label, queryRange: query_range, prometheusEndpointPath: prometheus_endpoint_path, metricId: uniqMetricsId({ metric_id, id }), diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js index d74447dd566..b2636f910fe 100644 --- a/app/assets/javascripts/repository/router.js +++ b/app/assets/javascripts/repository/router.js @@ -12,7 +12,7 @@ export default function createRouter(base, baseRef) { base: joinPaths(gon.relative_url_root || '', base), routes: [ { - path: `(/-)?/tree/(${encodeURIComponent(baseRef)}|${baseRef})/:path*`, + path: `(/-)?/tree/(${encodeURIComponent(baseRef).replace(/%2F/g, '/')}|${baseRef})/:path*`, name: 'treePath', component: TreePage, props: route => ({ diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue new file mode 100644 index 00000000000..83b50b2f8eb --- /dev/null +++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue @@ -0,0 +1,23 @@ +<script> +import { GlNewButton } from '@gitlab/ui'; + +export default { + components: { + GlNewButton, + }, + props: { + saveable: { + type: Boolean, + required: false, + default: false, + }, + }, +}; +</script> +<template> + <div class="d-flex bg-light border-top justify-content-between align-items-center py-3 px-4"> + <gl-new-button variant="success" :disabled="!saveable"> + {{ __('Submit Changes') }} + </gl-new-button> + </div> +</template> diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue index f06d48ee4f5..80a55d5ee11 100644 --- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue +++ b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue @@ -3,27 +3,29 @@ import { mapState, mapGetters, mapActions } from 'vuex'; import { GlSkeletonLoader } from '@gitlab/ui'; import EditArea from './edit_area.vue'; +import Toolbar from './publish_toolbar.vue'; export default { components: { EditArea, GlSkeletonLoader, + Toolbar, }, computed: { ...mapState(['content', 'isLoadingContent']), - ...mapGetters(['isContentLoaded']), + ...mapGetters(['isContentLoaded', 'contentChanged']), }, mounted() { this.loadContent(); }, methods: { - ...mapActions(['loadContent']), + ...mapActions(['loadContent', 'setContent']), }, }; </script> <template> - <div class="d-flex justify-content-center h-100"> - <div v-if="isLoadingContent" class="w-50 h-50 mt-2"> + <div class="d-flex justify-content-center h-100 pt-2"> + <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" /> @@ -33,6 +35,13 @@ export default { <rect x="410" y="40" width="90" height="16" rx="4" /> </gl-skeleton-loader> </div> - <edit-area v-if="isContentLoaded" class="w-75 h-100 shadow-none" :value="content" /> + <div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column"> + <edit-area + class="w-75 h-100 shadow-none align-self-center" + :value="content" + @input="setContent" + /> + <toolbar :saveable="contentChanged" /> + </div> </div> </template> diff --git a/app/assets/javascripts/static_site_editor/store/actions.js b/app/assets/javascripts/static_site_editor/store/actions.js index 192345f3749..141148de1e0 100644 --- a/app/assets/javascripts/static_site_editor/store/actions.js +++ b/app/assets/javascripts/static_site_editor/store/actions.js @@ -15,4 +15,8 @@ export const loadContent = ({ commit, state: { sourcePath, projectId } }) => { }); }; +export const setContent = ({ commit }, content) => { + commit(mutationTypes.SET_CONTENT, content); +}; + export default () => {}; diff --git a/app/assets/javascripts/static_site_editor/store/getters.js b/app/assets/javascripts/static_site_editor/store/getters.js index 8baa2941594..41256201c26 100644 --- a/app/assets/javascripts/static_site_editor/store/getters.js +++ b/app/assets/javascripts/static_site_editor/store/getters.js @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export -export const isContentLoaded = ({ content }) => Boolean(content); +export const isContentLoaded = ({ originalContent }) => Boolean(originalContent); +export const contentChanged = ({ originalContent, content }) => originalContent !== content; diff --git a/app/assets/javascripts/static_site_editor/store/mutation_types.js b/app/assets/javascripts/static_site_editor/store/mutation_types.js index cbe51180541..2bb201f5d24 100644 --- a/app/assets/javascripts/static_site_editor/store/mutation_types.js +++ b/app/assets/javascripts/static_site_editor/store/mutation_types.js @@ -1,3 +1,4 @@ export const LOAD_CONTENT = 'loadContent'; export const RECEIVE_CONTENT_SUCCESS = 'receiveContentSuccess'; export const RECEIVE_CONTENT_ERROR = 'receiveContentError'; +export const SET_CONTENT = 'setContent'; diff --git a/app/assets/javascripts/static_site_editor/store/mutations.js b/app/assets/javascripts/static_site_editor/store/mutations.js index 88cb74d2b11..8b8bacf35c2 100644 --- a/app/assets/javascripts/static_site_editor/store/mutations.js +++ b/app/assets/javascripts/static_site_editor/store/mutations.js @@ -8,8 +8,12 @@ export default { state.isLoadingContent = false; state.title = title; state.content = content; + state.originalContent = content; }, [types.RECEIVE_CONTENT_ERROR](state) { state.isLoadingContent = false; }, + [types.SET_CONTENT](state, content) { + state.content = content; + }, }; diff --git a/app/assets/javascripts/static_site_editor/store/state.js b/app/assets/javascripts/static_site_editor/store/state.js index b68e73f06f5..1ae11b3343d 100644 --- a/app/assets/javascripts/static_site_editor/store/state.js +++ b/app/assets/javascripts/static_site_editor/store/state.js @@ -3,7 +3,9 @@ const createState = (initialState = {}) => ({ sourcePath: null, isLoadingContent: false, + isSavingChanges: false, + originalContent: '', content: '', title: '', diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb index b5d67ef8e96..d3ff870e36a 100644 --- a/app/models/ci/build_dependencies.rb +++ b/app/models/ci/build_dependencies.rb @@ -67,7 +67,6 @@ module Ci end def from_needs(scope) - return scope unless Feature.enabled?(:ci_dag_support, project, default_enabled: true) return scope unless processable.scheduling_type_dag? needs_names = processable.needs.artifacts.select(:name) diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 55518f32316..4bc8f26ec92 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -25,8 +25,6 @@ module Ci end def self.select_with_aggregated_needs(project) - return all unless Feature.enabled?(:ci_dag_support, project, default_enabled: true) - aggregated_needs_names = Ci::BuildNeed .scoped_build .select("ARRAY_AGG(name)") diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2edb1a45100..b9acb539404 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -555,22 +555,28 @@ class MergeRequest < ApplicationRecord end end + def diff_stats + return unless diff_refs + + strong_memoize(:diff_stats) do + project.repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha) + end + end + def diff_size # Calling `merge_request_diff.diffs.real_size` will also perform # highlighting, which we don't need here. - merge_request_diff&.real_size || diffs.real_size + merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size end def modified_paths(past_merge_request_diff: nil) - diffs = if past_merge_request_diff - past_merge_request_diff - elsif compare - compare - else - self.merge_request_diff - end - - diffs.modified_paths + if past_merge_request_diff + past_merge_request_diff.modified_paths + elsif compare + diff_stats&.paths || compare.modified_paths + else + merge_request_diff.modified_paths + end end def new_paths diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 821de70f9d9..dbf600cf0df 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -267,7 +267,7 @@ class Snippet < ApplicationRecord def repository_size_checker strong_memoize(:repository_size_checker) do ::Gitlab::RepositorySizeChecker.new( - current_size_proc: -> { repository._uncached_size.megabytes }, + current_size_proc: -> { repository.size.megabytes }, limit: Gitlab::CurrentSettings.snippet_size_limit ) end diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb index 83ba70e8437..2e84f914db3 100644 --- a/app/services/ci/compare_reports_base_service.rb +++ b/app/services/ci/compare_reports_base_service.rb @@ -8,7 +8,8 @@ module Ci # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 class CompareReportsBaseService < ::BaseService def execute(base_pipeline, head_pipeline) - comparer = comparer_class.new(get_report(base_pipeline), get_report(head_pipeline)) + comparer = build_comparer(base_pipeline, head_pipeline) + { status: :parsed, key: key(base_pipeline, head_pipeline), @@ -28,6 +29,12 @@ module Ci data&.fetch(:key, nil) == key(base_pipeline, head_pipeline) end + protected + + def build_comparer(base_pipeline, head_pipeline) + comparer_class.new(get_report(base_pipeline), get_report(head_pipeline)) + end + private def key(base_pipeline, head_pipeline) diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index 55846c3cb5c..2a1bf15b9a3 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -93,7 +93,7 @@ module Ci end def processable_status(processable) - if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && processable.scheduling_type_dag? + if processable.scheduling_type_dag? # Processable uses DAG, get status of all dependent needs @collection.status_for_names(processable.aggregated_needs_names.to_a) else diff --git a/app/services/ci/pipeline_processing/legacy_processing_service.rb b/app/services/ci/pipeline_processing/legacy_processing_service.rb index 8d7b80282fc..c471f7f0011 100644 --- a/app/services/ci/pipeline_processing/legacy_processing_service.rb +++ b/app/services/ci/pipeline_processing/legacy_processing_service.rb @@ -43,8 +43,6 @@ module Ci end def process_dag_builds_without_needs - return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true) - created_processables.scheduling_type_dag.without_needs.each do |build| process_build(build, 'success') end @@ -52,7 +50,6 @@ module Ci def process_dag_builds_with_needs(trigger_build_ids) return false unless trigger_build_ids.present? - return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true) # we find processables that are dependent: # 1. because of current dependency, @@ -110,11 +107,7 @@ module Ci end def created_stage_scheduled_processables - if Feature.enabled?(:ci_dag_support, project, default_enabled: true) - created_processables.scheduling_type_stage - else - created_processables - end + created_processables.scheduling_type_stage end def created_processables diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 5636df7d5e0..f6255dac7cf 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -65,7 +65,7 @@ .dropdown-menu.dropdown-menu-right = render 'layouts/header/help_dropdown' - if header_link?(:user_dropdown) - %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) } + %li.nav-item.header-user.js-nav-user-dropdown.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) } = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = render_if_exists 'layouts/header/user_notification_dot', project: project, namespace: group diff --git a/app/views/projects/static_site_editor/show.html.haml b/app/views/projects/static_site_editor/show.html.haml index 574cdd0bf88..9ccc54e6d51 100644 --- a/app/views/projects/static_site_editor/show.html.haml +++ b/app/views/projects/static_site_editor/show.html.haml @@ -1 +1 @@ -#static-site-editor{ data: {} } +#static-site-editor{ data: { project_id: '8', path: 'README.md' } } diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 5178fabb2d8..ddf112e648c 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -77,12 +77,8 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker return false unless user - # At the moment, we only expires the repository caches. - # In the future we might need to call ProjectCacheWorker - # (or the custom class we create) to update the snippet - # repository size or any other key. - # We might also need to update the repository statistics. expire_caches(post_received, snippet.repository) + snippet.repository.expire_statistics_caches end # Expire the repository status, branch, and tag cache once per push. diff --git a/changelogs/unreleased/11631-dependency-proxy-purge-api.yml b/changelogs/unreleased/11631-dependency-proxy-purge-api.yml new file mode 100644 index 00000000000..2e2f0c32074 --- /dev/null +++ b/changelogs/unreleased/11631-dependency-proxy-purge-api.yml @@ -0,0 +1,5 @@ +--- +title: Add an endpoint to allow group admin users to purge the dependency proxy for a group +merge_request: 27843 +author: +type: added diff --git a/changelogs/unreleased/195739.yml b/changelogs/unreleased/195739.yml new file mode 100644 index 00000000000..70db469e5b6 --- /dev/null +++ b/changelogs/unreleased/195739.yml @@ -0,0 +1,5 @@ +--- +title: Update query labels dynamically for embedded charts +merge_request: 29034 +author: +type: other diff --git a/changelogs/unreleased/id-improve-modified-paths-performance.yml b/changelogs/unreleased/id-improve-modified-paths-performance.yml new file mode 100644 index 00000000000..9ad15ae4b6f --- /dev/null +++ b/changelogs/unreleased/id-improve-modified-paths-performance.yml @@ -0,0 +1,5 @@ +--- +title: Use diff-stats for calculating raw diffs modified paths +merge_request: 29134 +author: +type: performance diff --git a/changelogs/unreleased/id-remove-blobs_fetch_in_batches-feature-flag.yml b/changelogs/unreleased/id-remove-blobs_fetch_in_batches-feature-flag.yml new file mode 100644 index 00000000000..9f16beacb05 --- /dev/null +++ b/changelogs/unreleased/id-remove-blobs_fetch_in_batches-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove blobs_fetch_in_batches feature flag +merge_request: 29069 +author: +type: added diff --git a/changelogs/unreleased/ph-211585-fixWhitespaceToggleNotShowingCorrectDiff.yml b/changelogs/unreleased/ph-211585-fixWhitespaceToggleNotShowingCorrectDiff.yml new file mode 100644 index 00000000000..125b048ffa4 --- /dev/null +++ b/changelogs/unreleased/ph-211585-fixWhitespaceToggleNotShowingCorrectDiff.yml @@ -0,0 +1,5 @@ +--- +title: Fixed whitespace toggle not showing the correct diff +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/remove-additional-artifact-config-validation.yml b/changelogs/unreleased/remove-additional-artifact-config-validation.yml new file mode 100644 index 00000000000..6683b69898c --- /dev/null +++ b/changelogs/unreleased/remove-additional-artifact-config-validation.yml @@ -0,0 +1,5 @@ +--- +title: Validate dependency on job generating a CI config when using dynamic child pipelines +merge_request: 28901 +author: +type: added diff --git a/changelogs/unreleased/remove_ci_dag_support_feature_flag.yml b/changelogs/unreleased/remove_ci_dag_support_feature_flag.yml new file mode 100644 index 00000000000..c4b639dc67d --- /dev/null +++ b/changelogs/unreleased/remove_ci_dag_support_feature_flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove `ci_dag_support` feature flag +merge_request: 28863 +author: Lee Tickett +type: added diff --git a/changelogs/unreleased/update-gitlab-elasticsearch-indexer.yml b/changelogs/unreleased/update-gitlab-elasticsearch-indexer.yml new file mode 100644 index 00000000000..334686165c5 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-elasticsearch-indexer.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Elasticsearch Indexer +merge_request: 29256 +author: +type: other diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index c7d1e745fab..bc7eff9d181 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -66,6 +66,8 @@ - 1 - - delete_user - 1 +- - dependency_proxy + - 1 - - deployment - 3 - - design_management_new_version diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md index 7e697e8dd81..a1f511fe2a5 100644 --- a/doc/administration/geo/replication/datatypes.md +++ b/doc/administration/geo/replication/datatypes.md @@ -73,7 +73,7 @@ for Wiki and Design Repository cases. GitLab stores files and blobs such as Issue attachments or LFS objects into either: - The filesystem in a specific location. -- An Object Storage solution. Object Storage solutions can be: +- An [Object Storage](../../object_storage.md) solution. Object Storage solutions can be: - Cloud based like Amazon S3 Google Cloud Storage. - Hosted by you (like MinIO). - A Storage Appliance that exposes an Object Storage-compatible API. diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md index db8d26b3865..ffd44282b23 100644 --- a/doc/administration/geo/replication/object_storage.md +++ b/doc/administration/geo/replication/object_storage.md @@ -12,6 +12,8 @@ To have: - GitLab manage replication, follow [Enabling GitLab replication](#enabling-gitlab-managed-object-storage-replication). - Third-party services manage replication, follow [Third-party replication services](#third-party-replication-services). +[Read more about using object storage with GitLab](../../object_storage.md). + ## Enabling GitLab managed object storage replication > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/10586) in GitLab 12.4. diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index c45388087ab..a9a13062a25 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -92,6 +92,8 @@ Use an object storage option like AWS S3 to store job artifacts. DANGER: **Danger:** If you configure GitLab to store CI logs and artifacts on object storage, you must also enable [incremental logging](job_logs.md#new-incremental-logging-architecture). Otherwise, job logs will disappear or not be saved. +[Read more about using object storage with GitLab](object_storage.md). + #### Object Storage Settings For source installations the following settings are nested under `artifacts:` and then `object_store:`. On Omnibus GitLab installs they are prefixed by `artifacts_object_store_`. diff --git a/doc/administration/lfs/index.md b/doc/administration/lfs/index.md index 10ff15b1ff4..71c1ae22305 100644 --- a/doc/administration/lfs/index.md +++ b/doc/administration/lfs/index.md @@ -61,6 +61,8 @@ You can also use external object storage in a private local network. For example GitLab provides two different options for the uploading mechanism: "Direct upload" and "Background upload". +[Read more about using object storage with GitLab](../object_storage.md). + **Option 1. Direct upload** 1. User pushes an `lfs` file to the GitLab instance diff --git a/doc/administration/merge_request_diffs.md b/doc/administration/merge_request_diffs.md index fd1a425d6b1..795933e2772 100644 --- a/doc/administration/merge_request_diffs.md +++ b/doc/administration/merge_request_diffs.md @@ -68,6 +68,8 @@ Instead of storing the external diffs on disk, we recommended the use of an obje store like AWS S3 instead. This configuration relies on valid AWS credentials to be configured already. +[Read more about using object storage with GitLab](object_storage.md). + ## Object Storage Settings For source installations, these settings are nested under `external_diffs:` and diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index 55ec66112d2..80305c89e81 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -21,9 +21,6 @@ Object storage options that GitLab has tested, or is aware of customers using in For configuring GitLab to use Object Storage refer to the following guides: -1. Make sure the [`git` user home directory](https://docs.gitlab.com/omnibus/settings/configuration.html#moving-the-home-directory-for-a-user) is on local disk. -1. Configure [database lookup of SSH keys](operations/fast_ssh_key_lookup.md) - to eliminate the need for a shared `authorized_keys` file. 1. Configure [object storage for backups](../raketasks/backup_restore.md#uploading-backups-to-a-remote-cloud-storage). 1. Configure [object storage for job artifacts](job_artifacts.md#using-object-storage) including [incremental logging](job_logs.md#new-incremental-logging-architecture). @@ -36,6 +33,19 @@ For configuring GitLab to use Object Storage refer to the following guides: 1. Configure [object storage for Dependency Proxy](packages/dependency_proxy.md#using-object-storage) (optional feature). **(PREMIUM ONLY)** 1. Configure [object storage for Pseudonymizer](pseudonymizer.md#configuration) (optional feature). **(ULTIMATE ONLY)** 1. Configure [object storage for autoscale Runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional - for improved performance). +1. Configure [object storage for Terraform state files](terraform_state.md#using-object-storage-core-only) + +### Other alternatives to filesystem storage + +If you're working to [scale out](scaling/index.md) your GitLab implementation, +or add [fault tolerance and redundancy](high_availability/README.md) you may be +looking at removing dependencies on block or network filesystems. +See the following guides and +[note that Pages requires disk storage](#gitlab-pages-requires-nfs): + +1. Make sure the [`git` user home directory](https://docs.gitlab.com/omnibus/settings/configuration.html#moving-the-home-directory-for-a-user) is on local disk. +1. Configure [database lookup of SSH keys](operations/fast_ssh_key_lookup.md) + to eliminate the need for a shared `authorized_keys` file. ## Warnings, limitations, and known issues @@ -67,8 +77,9 @@ with the Fog library that GitLab uses. Symptoms include: ### GitLab Pages requires NFS -If you're working to [scale out](high_availability/README.md) your GitLab implementation and -one of your requirements is [GitLab Pages](../user/project/pages/index.md) this currently requires +If you're working to add more GitLab servers for [scaling](scaling/index.md) or +[fault tolerance](high_availability/README.md) and one of your requirements +is [GitLab Pages](../user/project/pages/index.md) this currently requires NFS. There is [work in progress](https://gitlab.com/gitlab-org/gitlab-pages/issues/196) to remove this dependency. In the future, GitLab Pages may use [object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/208135). diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index b940cb6933b..aaf1ca29084 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -367,6 +367,8 @@ The different supported drivers are: Read more about the individual driver's config options in the [Docker Registry docs](https://docs.docker.com/registry/configuration/#storage). +[Read more about using object storage with GitLab](../object_storage.md). + CAUTION: **Warning:** GitLab will not backup Docker images that are not stored on the filesystem. Remember to enable backups with your object storage provider if desired. diff --git a/doc/administration/packages/dependency_proxy.md b/doc/administration/packages/dependency_proxy.md index ff3c24d6162..ec2020c26de 100644 --- a/doc/administration/packages/dependency_proxy.md +++ b/doc/administration/packages/dependency_proxy.md @@ -77,7 +77,9 @@ To change the local storage path: ### Using object storage Instead of relying on the local storage, you can use an object storage to -upload the blobs of the dependency proxy: +store the blobs of the dependency proxy. + +[Read more about using object storage with GitLab](../object_storage.md). **Omnibus GitLab installations** diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index 536b6a5f246..d14726d33de 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -86,7 +86,9 @@ To change the local storage path: ### Using object storage Instead of relying on the local storage, you can use an object storage to -upload packages: +store packages. + +[Read more about using object storage with GitLab](../object_storage.md). **Omnibus GitLab installations** diff --git a/doc/administration/pseudonymizer.md b/doc/administration/pseudonymizer.md index 3cf0e96d18f..36bb446da78 100644 --- a/doc/administration/pseudonymizer.md +++ b/doc/administration/pseudonymizer.md @@ -26,6 +26,8 @@ To configure the pseudonymizer, you need to: Alternatively, you can use an absolute file path. - Use an object storage and specify the connection parameters in the `pseudonymizer.upload.connection` configuration option. +[Read more about using object storage with GitLab](object_storage.md). + **For Omnibus installations:** 1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with diff --git a/doc/administration/raketasks/uploads/migrate.md b/doc/administration/raketasks/uploads/migrate.md index 6dae9b71e1f..d2823847a89 100644 --- a/doc/administration/raketasks/uploads/migrate.md +++ b/doc/administration/raketasks/uploads/migrate.md @@ -7,6 +7,8 @@ After [configuring the object storage](../../uploads.md#using-object-storage-cor >**Note:** All of the processing will be done in a background worker and requires **no downtime**. +[Read more about using object storage with GitLab](../../object_storage.md). + ### All-in-one Rake task GitLab provides a wrapper Rake task that migrates all uploaded files - avatars, diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md index c684178f13e..0956edaf252 100644 --- a/doc/administration/terraform_state.md +++ b/doc/administration/terraform_state.md @@ -51,6 +51,8 @@ Instead of storing Terraform state files on disk, we recommend the use of an obj store that is S3-compatible instead. This configuration relies on valid credentials to be configured already. +[Read more about using object storage with GitLab](object_storage.md). + ### Object storage settings The following settings are: diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 45cffb64671..f29deba3d40 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -55,6 +55,8 @@ If you don't want to use the local disk where GitLab is installed to store the uploads, you can use an object storage provider like AWS S3 instead. This configuration relies on valid AWS credentials to be configured already. +[Read more about using object storage with GitLab](object_storage.md). + ## Object Storage Settings For source installations the following settings are nested under `uploads:` and then `object_store:`. On Omnibus GitLab installs they are prefixed by `uploads_object_store_`. diff --git a/doc/api/dependency_proxy.md b/doc/api/dependency_proxy.md new file mode 100644 index 00000000000..a379f1481c1 --- /dev/null +++ b/doc/api/dependency_proxy.md @@ -0,0 +1,21 @@ +# Dependency Proxy API **(PREMIUM)** + +## Purge the dependency proxy for a group + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11631) in GitLab 12.10. + +Deletes the cached blobs for a group. This endpoint requires group admin access. + +```plaintext +DELETE /groups/:id/dependency_proxy/cache +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | + +Example request: + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/dependency_proxy/cache" +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 959b263c301..f0b65b9ac6a 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -162,7 +162,7 @@ When the user is authenticated and `simple` is not set this returns something li "merge_method": "merge", "autoclose_referenced_issues": true, "suggestion_commit_message": null, - "marked_for_deletion_at": "2020-04-03", + "marked_for_deletion_at": "2020-04-03", // to be deprecated in GitLab 13.0 in favor of marked_for_deletion_on "marked_for_deletion_on": "2020-04-03", "statistics": { "commit_count": 37, @@ -287,6 +287,9 @@ When the user is authenticated and `simple` is not set this returns something li ] ``` +NOTE: **Note:** +For users on GitLab [Silver, Premium, or higher](https://about.gitlab.com/pricing/) the `marked_for_deletion_at` attribute will be deprecated in GitLab 13.0 in favor of the `marked_for_deletion_on` attribute. + Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see the `approvals_before_merge` parameter: @@ -408,7 +411,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo "merge_method": "merge", "autoclose_referenced_issues": true, "suggestion_commit_message": null, - "marked_for_deletion_at": "2020-04-03", + "marked_for_deletion_at": "2020-04-03", // to be deprecated in GitLab 13.0 in favor of marked_for_deletion_on "marked_for_deletion_on": "2020-04-03", "statistics": { "commit_count": 37, @@ -874,7 +877,7 @@ GET /projects/:id "service_desk_address": null, "autoclose_referenced_issues": true, "suggestion_commit_message": null, - "marked_for_deletion_at": "2020-04-03", + "marked_for_deletion_at": "2020-04-03", // to be deprecated in GitLab 13.0 in favor of marked_for_deletion_on "marked_for_deletion_on": "2020-04-03", "statistics": { "commit_count": 37, diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md index b6b53737dde..d4b87648f49 100644 --- a/doc/ci/directed_acyclic_graph/index.md +++ b/doc/ci/directed_acyclic_graph/index.md @@ -4,7 +4,7 @@ type: reference # Directed Acyclic Graph -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47063) in GitLab 12.2 (enabled by `ci_dag_support` feature flag). +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47063) in GitLab 12.2. A [directed acyclic graph](https://www.techopedia.com/definition/5739/directed-acyclic-graph-dag) can be used in the context of a CI/CD pipeline to build relationships between jobs such that diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md index b39e0b6e540..2bc897901fa 100644 --- a/doc/ci/parent_child_pipelines.md +++ b/doc/ci/parent_child_pipelines.md @@ -136,12 +136,11 @@ your own script to generate a YAML file, which is then [used to trigger a child This technique can be very powerful in generating pipelines targeting content that changed or to build a matrix of targets and architectures. +In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail. +This is [resolved in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/209070). + ## Limitations A parent pipeline can trigger many child pipelines, but a child pipeline cannot trigger further child pipelines. See the [related issue](https://gitlab.com/gitlab-org/gitlab/issues/29651) for discussion on possible future improvements. - -When triggering dynamic child pipelines, if the job containing the CI config artifact is not a predecessor of the -trigger job, the child pipeline will fail to be created, causing also the parent pipeline to fail. -In the future we want to validate the trigger job's dependencies [at the time the parent pipeline is created](https://gitlab.com/gitlab-org/gitlab/-/issues/209070) rather than when the child pipeline is created. diff --git a/doc/install/aws/img/choose_ami.png b/doc/install/aws/img/choose_ami.png Binary files differdeleted file mode 100644 index a07d42dd6fb..00000000000 --- a/doc/install/aws/img/choose_ami.png +++ /dev/null diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index a88e8b0c310..6708e40abb4 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -555,7 +555,7 @@ In `/etc/ssh/sshd_config` update the following: #### Amazon S3 object storage -Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. For instructions on how to configure each of these, please see [Cloud Object Storage](../../administration/high_availability/object_storage.md). +Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our [documentation includes configuration instructions](../../administration/object_storage.md) for each of these, and other information about using object storage with GitLab. Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file. @@ -580,90 +580,55 @@ On the EC2 dashboard: Now we have a custom AMI that we'll use to create our launch configuration the next step. -## Deploying GitLab inside an auto scaling group +## Deploy GitLab inside an auto scaling group -We'll use AWS's wizard to deploy GitLab and then SSH into the instance to -configure the PostgreSQL and Redis connections. +### Create a launch configuration -The Auto Scaling Group option is available through the EC2 dashboard on the left -sidebar. - -1. Click **Create Auto Scaling group**. -1. Create a new launch configuration. - -### Choose the AMI - -Choose the AMI: - -1. Go to the Community AMIs and search for `GitLab EE <version>` - where `<version>` the latest version as seen on the - [releases page](https://about.gitlab.com/releases/). - - ![Choose AMI](img/choose_ami.png) - -### Choose an instance type - -You should choose an instance type based on your workload. Consult -[the hardware requirements](../requirements.md#hardware-requirements) to choose -one that fits your needs (at least `c5.xlarge`, which is enough to accommodate 100 users): - -1. Choose the your instance type. -1. Click **Next: Configure Instance Details**. - -### Configure details - -In this step we'll configure some details: - -1. Enter a name (`gitlab-autoscaling`). -1. Select the IAM role we created. -1. Optionally, enable CloudWatch and the EBS-optimized instance settings. -1. In the "Advanced Details" section, set the IP address type to - "Do not assign a public IP address to any instances." -1. Click **Next: Add Storage**. - -### Add storage - -The root volume is 8GB by default and should be enough given that we won't store any data there. - -### Configure security group - -As a last step, configure the security group: - -1. Select the existing load balancer security group we have [created](#load-balancer). -1. Select **Review**. - -### Review and launch - -Now is a good time to review all the previous settings. When ready, click -**Create launch configuration** and select the SSH key pair with which you will -connect to the instance. - -### Create Auto Scaling Group - -We are now able to start creating our Auto Scaling Group: - -1. Give it a group name. -1. Set the group size to 2 as we want to always start with two instances. -1. Assign it our network VPC and add the **private subnets**. -1. In the "Advanced Details" section, choose to receive traffic from ELBs - and select our ELB. -1. Choose the ELB health check. -1. Click **Next: Configure scaling policies**. +From the EC2 dashboard: -This is the really great part of Auto Scaling; we get to choose when AWS -launches new instances and when it removes them. For this group we'll -scale between 2 and 4 instances where one instance will be added if CPU +1. Select **Launch Configurations** from the left menu and click **Create launch configuration**. +1. Select **My AMIs** from the left menu and select the `GitLab` custom AMI we created above. +1. Select an instance type best suited for your needs (at least a `c5.xlarge`) and click **Configure details**. +1. Enter a name for your launch configuration (we'll use `gitlab-ha-launch-config`). +1. **Do not** check **Request Spot Instance**. +1. From the **IAM Role** dropdown, pick the `GitLabAdmin` instance role we [created earlier](#creating-an-iam-ec2-instance-role-and-profile). +1. Leave the rest as defaults and click **Add Storage**. +1. The root volume is 8GiB by default and should be enough given that we won’t store any data there. Click **Configure Security Group**. +1. Check **Select and existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier. +1. Click **Review**, review your changes, and click **Create launch configuration**. +1. Acknowledge that you have access to the private key or create a new one. Click **Create launch configuration**. + +### Create an auto scaling group + +1. As soon as the launch configuration is created, you'll see an option to **Create an Auto Scaling group using this launch configuration**. Click that to start creating the auto scaling group. +1. Enter a **Group name** (we'll use `gitlab-auto-scaling-group`). +1. For **Group size**, enter the number of instances you want to start with (we'll enter `2`). +1. Select the `gitlab-vpc` from the **Network** dropdown. +1. Add both the private [subnets we created earlier](#subnets). +1. Expand the **Advanced Details** section and check the **Receive traffic from one or more load balancers** option. +1. From the **Classic Load Balancers** dropdown, Select the load balancer we created earlier. +1. For **Health Check Type**, select **ELB**. +1. We'll leave our **Health Check Grace Period** as the default `300` seconds. Click **Configure scaling policies**. +1. Check **Use scaling policies to adjust the capacity of this group**. +1. For this group we'll scale between 2 and 4 instances where one instance will be added if CPU utilization is greater than 60% and one instance is removed if it falls to less than 45%. ![Auto scaling group policies](img/policies.png) -Finally, configure notifications and tags as you see fit, and create the +1. Finally, configure notifications and tags as you see fit, review your changes, and create the auto scaling group. -You'll notice that after we save the configuration, AWS starts launching our two -instances in different AZs and without a public IP which is exactly what -we intended. +As the auto scaling group is created, you'll see your new instances spinning up in your EC2 dashboard. You'll also see the new instances added to your load balancer. Once the instances pass the heath check, they are ready to start receiving traffic from the load balancer. + +Since our instances are created by the auto scaling group, go back to your instances and terminate the [instance we created manually above](#install-gitlab). We only needed this instance to create our custom AMI. + +### Log in for the first time + +Using the domain name you used when setting up [DNS for the load balancer](#configure-dns-for-load-balancer), you should now be able to visit GitLab in your browser. The very first time you will be asked to set up a password +for the `root` user which has admin privileges on the GitLab instance. + +After you set it up, login with username `root` and the newly created password. ## Health check and monitoring with Prometheus diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 19065b27275..b0d90ea0345 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -309,6 +309,8 @@ In the example below we use Amazon S3 for storage, but Fog also lets you use for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is [also available](#uploading-to-locally-mounted-shares). +[Read more about using object storage with GitLab](../administration/object_storage.md). + #### Using Amazon S3 For Omnibus GitLab packages: diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md index 83f1d0f0de5..fcb7d8630f5 100644 --- a/doc/topics/git/partial_clone.md +++ b/doc/topics/git/partial_clone.md @@ -1,82 +1,115 @@ -# Partial Clone for Large Repositories - -CAUTION: **Alpha:** -Partial Clone is an experimental feature, and will significantly increase -Gitaly resource utilization when performing a partial clone, and decrease -performance of subsequent fetch operations. - -As Git repositories become very large, usability decreases as performance -decreases. One major challenge is cloning the repository, because Git will -download the entire repository including every commit and every version of -every object. This can be slow to transfer, and require large amounts of disk -space. - -Historically, performing a **shallow clone** -([`--depth`](https://www.git-scm.com/docs/git-clone#Documentation/git-clone.txt---depthltdepthgt)) -has been the only way to reduce the amount of data transferred when cloning -a Git repository. This does not, however, allow filtering by sub-tree which is -important for monolithic repositories containing many projects, or by object -size preventing unnecessary large objects being downloaded. +# Partial Clone + +As Git repositories grow in size, they can become cumbersome to work with +because of the large amount of history that must be downloaded, and the large +amount of disk space they require. [Partial clone](https://github.com/git/git/blob/master/Documentation/technical/partial-clone.txt) is a performance optimization that "allows Git to function without having a complete copy of the repository. The goal of this work is to allow Git better handle extremely large repositories." -Specifically, using partial clone, it should be possible for Git to natively -support: - -- large objects, instead of using [Git LFS](https://git-lfs.github.com/) -- enormous repositories - -Briefly, partial clone works by: +## Filter by file size -- excluding objects from being transferred when cloning or fetching a - repository using a new `--filter` flag -- downloading missing objects on demand +> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/2553) in GitLab 12.10. -Follow [Git for enormous repositories](https://gitlab.com/groups/gitlab-org/-/epics/773) for roadmap and updates. +Storing large binary files in Git is normally discouraged, because every large +file added will be downloaded by everyone who clones or fetches changes +thereafter. This is slow, if not a complete obstruction when working from a slow +or unreliable internet connection. -## Enabling partial clone +Using partial clone with a file size filter solves this problem, by excluding +troublesome large files from clones and fetches. When Git encounters a missing +file, it will be downloaded on demand. -> [Introduced](https://gitlab.com/gitlab-org/gitaly/issues/1553) in GitLab 12.4. - -To enable partial clone, use the [feature flags API](../../api/features.md). -For example: +When cloning a repository, use the `--filter=blob:limit=<size>` argument. For example, +to clone the repository excluding files larger than 1 megabyte: ```shell -curl --data "value=true" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/gitaly_upload_pack_filter +git clone --filter=blob:limit=1m git@gitlab.com:gitlab-com/www-gitlab-com.git ``` -Alternatively, flip the switch and enable the feature flag: - -```ruby -Feature.enable(:gitaly_upload_pack_filter) +This would produce the following output: + +```plaintext +Cloning into 'www-gitlab-com'... +remote: Enumerating objects: 832467, done. +remote: Counting objects: 100% (832467/832467), done. +remote: Compressing objects: 100% (207226/207226), done. +remote: Total 832467 (delta 585563), reused 826624 (delta 580099), pack-reused 0 +Receiving objects: 100% (832467/832467), 2.34 GiB | 5.05 MiB/s, done. +Resolving deltas: 100% (585563/585563), done. +remote: Enumerating objects: 146, done. +remote: Counting objects: 100% (146/146), done. +remote: Compressing objects: 100% (138/138), done. +remote: Total 146 (delta 8), reused 144 (delta 8), pack-reused 0 +Receiving objects: 100% (146/146), 471.45 MiB | 4.60 MiB/s, done. +Resolving deltas: 100% (8/8), done. +Updating files: 100% (13008/13008), done. +Filtering content: 100% (3/3), 131.24 MiB | 4.65 MiB/s, done. ``` -## Excluding objects by size - -Partial Clone allows large objects to be stored directly in the Git repository, -and be excluded from clones as desired by the user. This eliminates the error -prone process of deciding which objects should be stored in LFS or not. Using -partial clone, all files – large or small – may be treated the same. +The output will be longer because Git will first clone the repository excluding +files larger than 1 megabyte, and second download any missing large files needed +to checkout the `master` branch. + +When changing branches, Git may need to download more missing files. + +## Filter by object type + +> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/2553) in GitLab 12.10. + +For enormous repositories with millions of files, and long history, it may be +helpful to exclude all files and use in combination with `sparse-checkout` to +reduce the size of your working copy. + +```plaintext +# Clone the repo excluding all files +$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com/git +Cloning into 'www-gitlab-com'... +remote: Enumerating objects: 678296, done. +remote: Counting objects: 100% (678296/678296), done. +remote: Compressing objects: 100% (165915/165915), done. +remote: Total 678296 (delta 472342), reused 673292 (delta 467476), pack-reused 0 +Receiving objects: 100% (678296/678296), 81.06 MiB | 5.74 MiB/s, done. +Resolving deltas: 100% (472342/472342), done. +remote: Enumerating objects: 28, done. +remote: Counting objects: 100% (28/28), done. +remote: Compressing objects: 100% (25/25), done. +remote: Total 28 (delta 0), reused 12 (delta 0), pack-reused 0 +Receiving objects: 100% (28/28), 140.29 KiB | 341.00 KiB/s, done. +Updating files: 100% (28/28), done. + +$ cd www-gitlab-com + +$ git sparse-checkout init --cone + +$ git sparse-checkout add data +remote: Enumerating objects: 301, done. +remote: Counting objects: 100% (301/301), done. +remote: Compressing objects: 100% (292/292), done. +remote: Total 301 (delta 16), reused 102 (delta 9), pack-reused 0 +Receiving objects: 100% (301/301), 1.15 MiB | 608.00 KiB/s, done. +Resolving deltas: 100% (16/16), done. +Updating files: 100% (302/302), done. +``` -With the `uploadpack.allowFilter` and `uploadpack.allowAnySHA1InWant` options -enabled on the Git server: +For more details, see the Git documentation for +[`sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout). -```shell -# clone the repo, excluding blobs larger than 1 megabyte -git clone --filter=blob:limit=1m <url> +## Filter by file path -# in the checkout step of the clone, and any subsequent operations -# any blobs that are needed will be downloaded on demand -git checkout feature-branch -``` +CAUTION: **Experimental:** +Partial Clone using `sparse` filters is experimental, slow, and will +significantly increase Gitaly resource utilization when cloning and fetching. -## Excluding objects by path +Deeper integration between Partial Clone and Sparse Checkout is being explored +through the `--filter=sparse:oid=<blob-ish>` filter spec, but this is highly +experimental. This mode of filtering uses a format similar to a `.gitignore` +file to specify which files should be included when cloning and fetching. -Partial Clone allows clones to be filtered by path using a format similar to a -`.gitignore` file stored inside the repository. +For more details, see the Git documentation for +[`rev-list-options`](https://gitlab.com/gitlab-org/git/-/blob/9fadedd637b312089337d73c3ed8447e9f0aa775/Documentation/rev-list-options.txt#L735-780). With the `uploadpack.allowFilter` and `uploadpack.allowAnySHA1InWant` options enabled on the Git server: diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index ab2aad3b043..1adbbf51397 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -1116,3 +1116,22 @@ To avoid installation errors: kubectl get secrets/tiller-secret -n gitlab-managed-apps -o "jsonpath={.data['ca\.crt']}" | base64 -d > b.pem diff a.pem b.pem ``` + +### Error installing managed apps on EKS cluster + +If you're using a managed cluster on AWS EKS, and you are not able to install some of the managed +apps, consider checking the logs. + +You can check the logs by running following commands: + +```shell +kubectl get pods --all-namespaces +kubectl get services --all-namespaces +``` + +If you are getting the `Failed to assign an IP address to container` error, it's probably due to the +instance type you've specified in the AWS configuration. +The number and size of nodes might not have enough IP addresses to run or install those pods. + +For reference, all the AWS instance IP limits are found +[in this AWS repository on GitHub](https://github.com/aws/amazon-vpc-cni-k8s/blob/master/pkg/awsutils/vpc_ip_resource_limit.go) (search for `InstanceENIsAvailable`). diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index 26a7936f8fa..cfdcd9821fb 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -65,6 +65,13 @@ from GitLab. The blobs are kept forever, and there is no hard limit on how much data can be stored. +## Clearing the cache + +It is possible to use the GitLab API to purge the dependency proxy cache for a +given group to gain back disk space that may be taken up by image blobs that +are no longer needed. See the [dependency proxy API documentation](../../../api/dependency_proxy.md) +for more details. + ## Limitations The following limitations apply: diff --git a/lib/gitlab/ci/config/entry/include.rb b/lib/gitlab/ci/config/entry/include.rb index cd09d83b728..9c2e5f641d0 100644 --- a/lib/gitlab/ci/config/entry/include.rb +++ b/lib/gitlab/ci/config/entry/include.rb @@ -15,6 +15,14 @@ module Gitlab validations do validates :config, hash_or_string: true validates :config, allowed_keys: ALLOWED_KEYS + + validate do + next unless config.is_a?(Hash) + + if config[:artifact] && config[:job].blank? + errors.add(:config, "must specify the job where to fetch the artifact from") + end + end end end end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 764047dae6d..933504ea82f 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -142,6 +142,7 @@ module Gitlab validate_job_stage!(name, job) validate_job_dependencies!(name, job) validate_job_needs!(name, job) + validate_dynamic_child_pipeline_dependencies!(name, job) validate_job_environment!(name, job) end end @@ -163,37 +164,50 @@ module Gitlab def validate_job_dependencies!(name, job) return unless job[:dependencies] - stage_index = @stages.index(job[:stage]) - job[:dependencies].each do |dependency| - raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] + validate_job_dependency!(name, dependency) + end + end - dependency_stage_index = @stages.index(@jobs[dependency.to_sym][:stage]) + def validate_dynamic_child_pipeline_dependencies!(name, job) + return unless includes = job.dig(:trigger, :include) - unless dependency_stage_index.present? && dependency_stage_index < stage_index - raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" - end + Array(includes).each do |included| + next unless included.is_a?(Hash) + next unless dependency = included[:job] + + validate_job_dependency!(name, dependency) end end def validate_job_needs!(name, job) - return unless job.dig(:needs, :job) - - stage_index = @stages.index(job[:stage]) + return unless needs = job.dig(:needs, :job) - job.dig(:needs, :job).each do |need| - need_job_name = need[:name] + needs.each do |need| + validate_job_dependency!(name, need[:name], 'need') + end + end - raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym] + def validate_job_dependency!(name, dependency, dependency_type = 'dependency') + unless @jobs[dependency.to_sym] + raise ValidationError, "#{name} job: undefined #{dependency_type}: #{dependency}" + end - needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage]) + job_stage_index = stage_index(name) + dependency_stage_index = stage_index(dependency) - unless needs_stage_index.present? && needs_stage_index < stage_index - raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages" - end + # A dependency might be defined later in the configuration + # with a stage that does not exist + unless dependency_stage_index.present? && dependency_stage_index < job_stage_index + raise ValidationError, "#{name} job: #{dependency_type} #{dependency} is not defined in prior stages" end end + def stage_index(name) + stage = @jobs.dig(name.to_sym, :stage) + @stages.index(stage) + end + def validate_job_environment!(name, job) return unless job[:environment] return unless job[:environment].is_a?(Hash) diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 5579449bf57..1b49d356d29 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -86,12 +86,8 @@ module Gitlab # to the caller to limit the number of blobs and blob_size_limit. # def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE) - if Feature.enabled?(:blobs_fetch_in_batches, default_enabled: true) - blob_references.each_slice(BATCH_SIZE).flat_map do |refs| - repository.gitaly_blob_client.get_blobs(refs, blob_size_limit).to_a - end - else - repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a + blob_references.each_slice(BATCH_SIZE).flat_map do |refs| + repository.gitaly_blob_client.get_blobs(refs, blob_size_limit).to_a end end diff --git a/lib/gitlab/git/diff_stats_collection.rb b/lib/gitlab/git/diff_stats_collection.rb index 998c41497a2..7e49d79676e 100644 --- a/lib/gitlab/git/diff_stats_collection.rb +++ b/lib/gitlab/git/diff_stats_collection.rb @@ -22,6 +22,15 @@ module Gitlab @collection.map(&:path) end + def real_size + max_files = ::Commit.max_diff_options[:max_files] + if paths.size > max_files + "#{max_files}+" + else + paths.size.to_s + end + end + private def indexed_by_path diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po index 2ca8f20d2c2..d94ad80b1ad 100644 --- a/locale/de/gitlab.po +++ b/locale/de/gitlab.po @@ -2470,7 +2470,7 @@ msgid "AutoDevOps|Learn more in the %{link_to_documentation}" msgstr "Erfahre mehr in der %{link_to_documentation}" msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}" -msgstr "Die Auto-DevOps-Pipeline wurde aktiviert und wir verwendet, falls keine alternative CI-Konfigurationsdatei gefunden wurde. %{more_information_link}" +msgstr "Die Auto-DevOps-Pipeline wurde aktiviert und wird verwendet, falls keine alternative CI-Konfigurationsdatei gefunden wurde. %{more_information_link}" msgid "Autocomplete" msgstr "Autovervollständigung" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4ef30825233..3aedf132df0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19388,6 +19388,9 @@ msgstr "" msgid "Subkeys" msgstr "" +msgid "Submit Changes" +msgstr "" + msgid "Submit a review" msgstr "" @@ -20510,7 +20513,7 @@ msgstr "" msgid "There was an error while fetching value stream analytics duration median data." msgstr "" -msgid "There was an error while fetching value stream analytics summary data." +msgid "There was an error while fetching value stream analytics recent activity data." msgstr "" msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index ad05f27b325..c44feaf4b63 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -796,11 +796,13 @@ describe('DiffsStoreMutations', () => { it('sets showWhitespace', () => { const state = { showWhitespace: true, + diffFiles: ['test'], }; mutations[types.SET_SHOW_WHITESPACE](state, false); expect(state.showWhitespace).toBe(false); + expect(state.diffFiles).toEqual([]); }); }); diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js index 00b5b306d66..0a74799283a 100644 --- a/spec/frontend/header_spec.js +++ b/spec/frontend/header_spec.js @@ -1,53 +1,87 @@ import $ from 'jquery'; -import initTodoToggle from '~/header'; +import initTodoToggle, { initNavUserDropdownTracking } from '~/header'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; describe('Header', () => { - const todosPendingCount = '.todos-count'; - const fixtureTemplate = 'issues/open-issue.html'; + describe('Todos notification', () => { + const todosPendingCount = '.todos-count'; + const fixtureTemplate = 'issues/open-issue.html'; - function isTodosCountHidden() { - return $(todosPendingCount).hasClass('hidden'); - } + function isTodosCountHidden() { + return $(todosPendingCount).hasClass('hidden'); + } - function triggerToggle(newCount) { - $(document).trigger('todo:toggle', newCount); - } + function triggerToggle(newCount) { + $(document).trigger('todo:toggle', newCount); + } - preloadFixtures(fixtureTemplate); - beforeEach(() => { - initTodoToggle(); - loadFixtures(fixtureTemplate); - }); + preloadFixtures(fixtureTemplate); + beforeEach(() => { + initTodoToggle(); + loadFixtures(fixtureTemplate); + }); - it('should update todos-count after receiving the todo:toggle event', () => { - triggerToggle(5); + it('should update todos-count after receiving the todo:toggle event', () => { + triggerToggle(5); - expect($(todosPendingCount).text()).toEqual('5'); - }); + expect($(todosPendingCount).text()).toEqual('5'); + }); - it('should hide todos-count when it is 0', () => { - triggerToggle(0); + it('should hide todos-count when it is 0', () => { + triggerToggle(0); - expect(isTodosCountHidden()).toEqual(true); - }); + expect(isTodosCountHidden()).toEqual(true); + }); + + it('should show todos-count when it is more than 0', () => { + triggerToggle(10); + + expect(isTodosCountHidden()).toEqual(false); + }); + + describe('when todos-count is 1000', () => { + beforeEach(() => { + triggerToggle(1000); + }); - it('should show todos-count when it is more than 0', () => { - triggerToggle(10); + it('should show todos-count', () => { + expect(isTodosCountHidden()).toEqual(false); + }); - expect(isTodosCountHidden()).toEqual(false); + it('should show 99+ for todos-count', () => { + expect($(todosPendingCount).text()).toEqual('99+'); + }); + }); }); - describe('when todos-count is 1000', () => { + describe('Track user dropdown open', () => { + let trackingSpy; + beforeEach(() => { - triggerToggle(1000); + setFixtures(` + <li class="js-nav-user-dropdown"> + <a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes + </a> + </li>`); + + trackingSpy = mockTracking('_category_', $('.js-nav-user-dropdown').element, jest.spyOn); + document.body.dataset.page = 'some:page'; + + initNavUserDropdownTracking(); }); - it('should show todos-count', () => { - expect(isTodosCountHidden()).toEqual(false); + afterEach(() => { + unmockTracking(); }); - it('should show 99+ for todos-count', () => { - expect($(todosPendingCount).text()).toEqual('99+'); + it('sends a tracking event when the dropdown is opened and contains Buy CI minutes link', () => { + $('.js-nav-user-dropdown').trigger('shown.bs.dropdown'); + + expect(trackingSpy).toHaveBeenCalledTimes(1); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'show_buy_ci_minutes', { + label: 'free', + property: 'user_dropdown', + }); }); }); }); diff --git a/spec/frontend/helpers/monitor_helper_spec.js b/spec/frontend/helpers/monitor_helper_spec.js index 0798ca580e2..f7163d496d2 100644 --- a/spec/frontend/helpers/monitor_helper_spec.js +++ b/spec/frontend/helpers/monitor_helper_spec.js @@ -56,6 +56,32 @@ describe('monitor helper', () => { expect(result.name).toEqual('brpop'); }); + it('supports a multi metric label template expression', () => { + const config = { + ...defaultConfig, + name: '', + }; + + const [result] = monitorHelper.makeDataSeries( + [ + { + metric: { + backend: 'HA Server', + frontend: 'BA Server', + app: 'prometheus', + instance: 'k8 cluster 1', + }, + values: series, + }, + ], + config, + ); + + expect(result.name).toBe( + 'backend: HA Server, frontend: BA Server, app: prometheus, instance: k8 cluster 1', + ); + }); + it('supports space-padded template expressions', () => { const config = { ...defaultConfig, diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index e1c8e694122..fcc5614850b 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -251,7 +251,7 @@ describe('mapToDashboardViewModel', () => { }; it('creates a metric', () => { - const dashboard = dashboardWithMetric({}); + const dashboard = dashboardWithMetric({ label: 'Panel Label' }); expect(getMappedMetric(dashboard)).toEqual({ label: expect.any(String), @@ -268,11 +268,11 @@ describe('mapToDashboardViewModel', () => { expect(getMappedMetric(dashboard).metricId).toEqual('1_http_responses'); }); - it('creates a metric with a default label', () => { + it('creates a metric without a default label', () => { const dashboard = dashboardWithMetric({}); expect(getMappedMetric(dashboard)).toMatchObject({ - label: defaultLabel, + label: undefined, }); }); diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index 8f3ac53c37a..6944b23558a 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -4,14 +4,15 @@ import createRouter from '~/repository/router'; describe('Repository router spec', () => { it.each` - path | component | componentName - ${'/'} | ${IndexPage} | ${'IndexPage'} - ${'/tree/master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/master/app/assets'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/123/app/assets'} | ${null} | ${'null'} - `('sets component as $componentName for path "$path"', ({ path, component }) => { - const router = createRouter('', 'master'); + path | branch | component | componentName + ${'/'} | ${'master'} | ${IndexPage} | ${'IndexPage'} + ${'/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/master/app/assets'} | ${'master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/feature/test-%23/app/assets'} | ${'feature/test-#'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/123/app/assets'} | ${'master'} | ${null} | ${'null'} + `('sets component as $componentName for path "$path"', ({ path, component, branch }) => { + const router = createRouter('', branch); const componentsForRoute = router.getMatchedComponents(path); diff --git a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js new file mode 100644 index 00000000000..55e30825621 --- /dev/null +++ b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlNewButton } from '@gitlab/ui'; + +import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; + +describe('Static Site Editor Toolbar', () => { + let wrapper; + + const buildWrapper = (propsData = {}) => { + wrapper = shallowMount(PublishToolbar, { + propsData: { + saveable: false, + ...propsData, + }, + }); + }; + + const findSaveChangesButton = () => wrapper.find(GlNewButton); + + beforeEach(() => { + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders Submit Changes button', () => { + expect(findSaveChangesButton().exists()).toBe(true); + }); + + it('disables Submit Changes button', () => { + expect(findSaveChangesButton().attributes('disabled')).toBe('true'); + }); + + describe('when saveable', () => { + it('enables Submit Changes button', () => { + buildWrapper({ saveable: true }); + + expect(findSaveChangesButton().attributes('disabled')).toBeFalsy(); + }); + }); +}); diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js index cde95d0a21e..ec984102250 100644 --- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js +++ b/spec/frontend/static_site_editor/components/static_site_editor_spec.js @@ -7,6 +7,7 @@ import createState from '~/static_site_editor/store/state'; import StaticSiteEditor from '~/static_site_editor/components/static_site_editor.vue'; import EditArea from '~/static_site_editor/components/edit_area.vue'; +import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; const localVue = createLocalVue(); @@ -16,18 +17,31 @@ describe('StaticSiteEditor', () => { let wrapper; let store; let loadContentActionMock; + let setContentActionMock; const buildStore = ({ initialState, getters } = {}) => { loadContentActionMock = jest.fn(); + setContentActionMock = jest.fn(); store = new Vuex.Store({ state: createState(initialState), getters: { isContentLoaded: () => false, + contentChanged: () => false, ...getters, }, actions: { loadContent: loadContentActionMock, + setContent: setContentActionMock, + }, + }); + }; + const buildContentLoadedStore = ({ initialState, getters } = {}) => { + buildStore({ + initialState, + getters: { + isContentLoaded: () => true, + ...getters, }, }); }; @@ -40,6 +54,8 @@ describe('StaticSiteEditor', () => { }; const findEditArea = () => wrapper.find(EditArea); + const findPublishToolbar = () => wrapper.find(PublishToolbar); + const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); beforeEach(() => { buildStore(); @@ -54,6 +70,10 @@ describe('StaticSiteEditor', () => { it('does not render edit area', () => { expect(findEditArea().exists()).toBe(false); }); + + it('does not render toolbar', () => { + expect(findPublishToolbar().exists()).toBe(false); + }); }); describe('when content is loaded', () => { @@ -68,19 +88,49 @@ describe('StaticSiteEditor', () => { expect(findEditArea().exists()).toBe(true); }); + it('does not render skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(false); + }); + it('passes page content to edit area', () => { expect(findEditArea().props('value')).toBe(content); }); + + it('renders toolbar', () => { + expect(findPublishToolbar().exists()).toBe(true); + }); + }); + + it('sets toolbar as saveable when content changes', () => { + buildContentLoadedStore({ + getters: { + contentChanged: () => true, + }, + }); + buildWrapper(); + + expect(findPublishToolbar().props('saveable')).toBe(true); }); - it('displays skeleton loader while loading content', () => { + it('displays skeleton loader when loading content', () => { buildStore({ initialState: { isLoadingContent: true } }); buildWrapper(); - expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true); + expect(findSkeletonLoader().exists()).toBe(true); }); it('dispatches load content action', () => { expect(loadContentActionMock).toHaveBeenCalled(); }); + + it('dispatches setContent action when edit area emits input event', () => { + const content = 'new content'; + + buildContentLoadedStore(); + buildWrapper(); + + findEditArea().vm.$emit('input', content); + + expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), content, undefined); + }); }); diff --git a/spec/frontend/static_site_editor/store/actions_spec.js b/spec/frontend/static_site_editor/store/actions_spec.js index 98d7d0d2c2d..4ad1e798ccd 100644 --- a/spec/frontend/static_site_editor/store/actions_spec.js +++ b/spec/frontend/static_site_editor/store/actions_spec.js @@ -73,4 +73,15 @@ describe('Static Site Editor Store actions', () => { }); }); }); + + describe('setContent', () => { + it('commits setContent mutation', () => { + testAction(actions.setContent, content, state, [ + { + type: mutationTypes.SET_CONTENT, + payload: content, + }, + ]); + }); + }); }); diff --git a/spec/frontend/static_site_editor/store/getters_spec.js b/spec/frontend/static_site_editor/store/getters_spec.js index 8800216f3b0..1b482db9366 100644 --- a/spec/frontend/static_site_editor/store/getters_spec.js +++ b/spec/frontend/static_site_editor/store/getters_spec.js @@ -1,15 +1,29 @@ import createState from '~/static_site_editor/store/state'; -import { isContentLoaded } from '~/static_site_editor/store/getters'; +import { isContentLoaded, contentChanged } from '~/static_site_editor/store/getters'; import { sourceContent as content } from '../mock_data'; describe('Static Site Editor Store getters', () => { describe('isContentLoaded', () => { - it('returns true when content is not empty', () => { - expect(isContentLoaded(createState({ content }))).toBe(true); + it('returns true when originalContent is not empty', () => { + expect(isContentLoaded(createState({ originalContent: content }))).toBe(true); }); - it('returns false when content is empty', () => { - expect(isContentLoaded(createState({ content: '' }))).toBe(false); + it('returns false when originalContent is empty', () => { + expect(isContentLoaded(createState({ originalContent: '' }))).toBe(false); + }); + }); + + describe('contentChanged', () => { + it('returns true when content and originalContent are different', () => { + const state = createState({ content, originalContent: 'something else' }); + + expect(contentChanged(state)).toBe(true); + }); + + it('returns false when content and originalContent are the same', () => { + const state = createState({ content, originalContent: content }); + + expect(contentChanged(state)).toBe(false); }); }); }); diff --git a/spec/frontend/static_site_editor/store/mutations_spec.js b/spec/frontend/static_site_editor/store/mutations_spec.js index c7055fbb2f0..db3a1081af5 100644 --- a/spec/frontend/static_site_editor/store/mutations_spec.js +++ b/spec/frontend/static_site_editor/store/mutations_spec.js @@ -35,8 +35,9 @@ describe('Static Site Editor Store mutations', () => { expect(state.title).toBe(payload.title); }); - it('sets content', () => { + it('sets originalContent and content', () => { expect(state.content).toBe(payload.content); + expect(state.originalContent).toBe(payload.content); }); }); @@ -49,4 +50,12 @@ describe('Static Site Editor Store mutations', () => { expect(state.isLoadingContent).toBe(false); }); }); + + describe('setContent', () => { + it('sets content', () => { + mutations[types.SET_CONTENT](state, content); + + expect(state.content).toBe(content); + }); + }); }); diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb new file mode 100644 index 00000000000..bab11f26fa1 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::Ci::Config::Entry::Include do + subject(:include_entry) { described_class.new(config) } + + describe 'validations' do + before do + include_entry.compose! + end + + context 'when value is a string' do + let(:config) { 'test.yml' } + + it { is_expected.to be_valid } + end + + context 'when value is hash' do + context 'when using not allowed keys' do + let(:config) do + { not_allowed: 'key' } + end + + it { is_expected.not_to be_valid } + end + + context 'when using "local"' do + let(:config) { { local: 'test.yml' } } + + it { is_expected.to be_valid } + end + + context 'when using "file"' do + let(:config) { { file: 'test.yml' } } + + it { is_expected.to be_valid } + end + + context 'when using "template"' do + let(:config) { { template: 'test.yml' } } + + it { is_expected.to be_valid } + end + + context 'when using "artifact"' do + context 'and specifying "job"' do + let(:config) { { artifact: 'test.yml', job: 'generator' } } + + it { is_expected.to be_valid } + end + + context 'without "job"' do + let(:config) { { artifact: 'test.yml' } } + + it { is_expected.not_to be_valid } + + it 'has specific error' do + expect(include_entry.errors) + .to include('include config must specify the job where to fetch the artifact from') + end + end + end + end + + context 'when value is something else' do + let(:config) { 123 } + + it { is_expected.not_to be_valid } + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 62adba4319e..70c3c5ab339 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2052,6 +2052,54 @@ module Gitlab end end + describe 'with parent-child pipeline' do + context 'when artifact and job are specified' do + let(:config) do + YAML.dump({ + build1: { stage: 'build', script: 'test' }, + test1: { stage: 'test', trigger: { + include: [{ artifact: 'generated.yml', job: 'build1' }] + } } + }) + end + + it { expect { subject }.not_to raise_error } + end + + context 'when job is not specified specified while artifact is' do + let(:config) do + YAML.dump({ + build1: { stage: 'build', script: 'test' }, + test1: { stage: 'test', trigger: { + include: [{ artifact: 'generated.yml' }] + } } + }) + end + + it do + expect { subject }.to raise_error( + described_class::ValidationError, + /include config must specify the job where to fetch the artifact from/) + end + end + + context 'when include is a string' do + let(:config) do + YAML.dump({ + build1: { stage: 'build', script: 'test' }, + test1: { + stage: 'test', + trigger: { + include: 'generated.yml' + } + } + }) + end + + it { expect { subject }.not_to raise_error } + end + end + describe "Error handling" do it "fails to parse YAML" do expect do diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index f25383ef416..06f9767d58b 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -301,26 +301,12 @@ describe Gitlab::Git::Blob, :seed_helper do stub_const('Gitlab::Git::Blob::BATCH_SIZE', 2) end - context 'blobs_fetch_in_batches is enabled' do - it 'fetches the blobs in batches' do - expect(client).to receive(:get_blobs).with(first_batch, limit).ordered - expect(client).to receive(:get_blobs).with(second_batch, limit).ordered - expect(client).to receive(:get_blobs).with(third_batch, limit).ordered + it 'fetches the blobs in batches' do + expect(client).to receive(:get_blobs).with(first_batch, limit).ordered + expect(client).to receive(:get_blobs).with(second_batch, limit).ordered + expect(client).to receive(:get_blobs).with(third_batch, limit).ordered - subject - end - end - - context 'blobs_fetch_in_batches is disabled' do - before do - stub_feature_flags(blobs_fetch_in_batches: false) - end - - it 'fetches the blobs in a single batch' do - expect(client).to receive(:get_blobs).with(blob_references, limit) - - subject - end + subject end end end diff --git a/spec/lib/gitlab/git/diff_stats_collection_spec.rb b/spec/lib/gitlab/git/diff_stats_collection_spec.rb index b07690ef39c..82d15a49062 100644 --- a/spec/lib/gitlab/git/diff_stats_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_stats_collection_spec.rb @@ -29,4 +29,16 @@ describe Gitlab::Git::DiffStatsCollection do expect(collection.paths).to eq %w[foo bar] end end + + describe '#real_size' do + it 'returns the number of modified files' do + expect(collection.real_size).to eq('2') + end + + it 'returns capped number when it is bigger than max_files' do + allow(::Commit).to receive(:max_diff_options).and_return(max_files: 1) + + expect(collection.real_size).to eq('1+') + end + end end diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb index 6db69d0f34c..8f2199ac360 100644 --- a/spec/models/ci/build_dependencies_spec.rb +++ b/spec/models/ci/build_dependencies_spec.rb @@ -96,14 +96,6 @@ describe Ci::BuildDependencies do end it { is_expected.to contain_exactly(build, rspec_test, staging) } - - context 'when ci_dag_support is disabled' do - before do - stub_feature_flags(ci_dag_support: false) - end - - it { is_expected.to contain_exactly(build, rspec_test, rubocop_test, staging) } - end end context 'when need artifacts are defined' do diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index e03f54aa728..e8ef7b29681 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -25,20 +25,6 @@ describe Ci::Processable do it 'returns all needs' do expect(with_aggregated_needs.first.aggregated_needs_names).to contain_exactly('test1', 'test2') end - - context 'with ci_dag_support disabled' do - before do - stub_feature_flags(ci_dag_support: false) - end - - it 'returns all processables' do - expect(with_aggregated_needs).to contain_exactly(processable) - end - - it 'returns empty needs' do - expect(with_aggregated_needs.first.aggregated_needs_names).to be_nil - end - end end context 'without needs' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index bfad1da1e21..50bb194ef71 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -874,7 +874,7 @@ describe MergeRequest do subject(:merge_request) { build(:merge_request) } before do - expect(diff).to receive(:modified_paths).and_return(paths) + allow(diff).to receive(:modified_paths).and_return(paths) end context 'when past_merge_request_diff is specified' do @@ -890,10 +890,29 @@ describe MergeRequest do let(:compare) { double(:compare) } let(:diff) { compare } - it 'returns affected file paths from compare' do + before do merge_request.compare = compare - expect(merge_request.modified_paths).to eq(paths) + expect(merge_request).to receive(:diff_stats).and_return(diff_stats) + end + + context 'and diff_stats are not present' do + let(:diff_stats) { nil } + + it 'returns affected file paths from compare' do + expect(merge_request.modified_paths).to eq(paths) + end + end + + context 'and diff_stats are present' do + let(:diff_stats) { double(:diff_stats) } + + it 'returns affected file paths from compare' do + diff_stats_path = double(:diff_stats_paths) + expect(diff_stats).to receive(:paths).and_return(diff_stats_path) + + expect(merge_request.modified_paths).to eq(diff_stats_path) + end end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 8c4d1951697..2061084d5ea 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -703,7 +703,7 @@ describe Snippet do let(:current_size) { 60 } before do - allow(subject.repository).to receive(:_uncached_size).and_return(current_size) + allow(subject.repository).to receive(:size).and_return(current_size) end it 'sets up size checker', :aggregate_failures do diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb index b4071d1b0fe..a76e83f2d60 100644 --- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb +++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb @@ -18,25 +18,10 @@ describe Ci::CreatePipelineService, '#execute' do before do project.add_developer(user) - stub_ci_pipeline_to_return_yaml_file + stub_ci_pipeline_yaml_file(config) end - describe 'child pipeline triggers' do - before do - stub_ci_pipeline_yaml_file <<~YAML - test: - script: rspec - - deploy: - variables: - CROSS: downstream - stage: deploy - trigger: - include: - - local: path/to/child.yml - YAML - end - + shared_examples 'successful creation' do it 'creates bridge jobs correctly' do pipeline = create_pipeline! @@ -48,58 +33,140 @@ describe Ci::CreatePipelineService, '#execute' do expect(bridge).to be_a Ci::Bridge expect(bridge.stage).to eq 'deploy' expect(pipeline.statuses).to match_array [test, bridge] - expect(bridge.options).to eq( - 'trigger' => { 'include' => [{ 'local' => 'path/to/child.yml' }] } - ) + expect(bridge.options).to eq(expected_bridge_options) expect(bridge.yaml_variables) .to include(key: 'CROSS', value: 'downstream', public: true) end end + shared_examples 'creation failure' do + it 'returns errors' do + pipeline = create_pipeline! + + expect(pipeline.errors.full_messages.first).to match(expected_error) + expect(pipeline.failure_reason).to eq 'config_error' + expect(pipeline).to be_persisted + expect(pipeline.status).to eq 'failed' + end + end + + describe 'child pipeline triggers' do + let(:config) do + <<~YAML + test: + script: rspec + deploy: + variables: + CROSS: downstream + stage: deploy + trigger: + include: + - local: path/to/child.yml + YAML + end + + it_behaves_like 'successful creation' do + let(:expected_bridge_options) do + { + 'trigger' => { + 'include' => [ + { 'local' => 'path/to/child.yml' } + ] + } + } + end + end + end + describe 'child pipeline triggers' do context 'when YAML is valid' do - before do - stub_ci_pipeline_yaml_file <<~YAML + let(:config) do + <<~YAML + test: + script: rspec + deploy: + variables: + CROSS: downstream + stage: deploy + trigger: + include: + - local: path/to/child.yml + YAML + end + + it_behaves_like 'successful creation' do + let(:expected_bridge_options) do + { + 'trigger' => { + 'include' => [ + { 'local' => 'path/to/child.yml' } + ] + } + } + end + end + + context 'when trigger:include is specified as a string' do + let(:config) do + <<~YAML test: script: rspec + deploy: + variables: + CROSS: downstream + stage: deploy + trigger: + include: path/to/child.yml + YAML + end + + it_behaves_like 'successful creation' do + let(:expected_bridge_options) do + { + 'trigger' => { + 'include' => 'path/to/child.yml' + } + } + end + end + end + context 'when trigger:include is specified as array of strings' do + let(:config) do + <<~YAML + test: + script: rspec deploy: variables: CROSS: downstream stage: deploy trigger: include: - - local: path/to/child.yml - YAML - end + - path/to/child.yml + - path/to/child2.yml + YAML + end - it 'creates bridge jobs correctly' do - pipeline = create_pipeline! - - test = pipeline.statuses.find_by(name: 'test') - bridge = pipeline.statuses.find_by(name: 'deploy') - - expect(pipeline).to be_persisted - expect(test).to be_a Ci::Build - expect(bridge).to be_a Ci::Bridge - expect(bridge.stage).to eq 'deploy' - expect(pipeline.statuses).to match_array [test, bridge] - expect(bridge.options).to eq( - 'trigger' => { 'include' => [{ 'local' => 'path/to/child.yml' }] } - ) - expect(bridge.yaml_variables) - .to include(key: 'CROSS', value: 'downstream', public: true) + it_behaves_like 'successful creation' do + let(:expected_bridge_options) do + { + 'trigger' => { + 'include' => ['path/to/child.yml', 'path/to/child2.yml'] + } + } + end + end end end - context 'when YAML is invalid' do + context 'when limit of includes is reached' do let(:config) do - { + YAML.dump({ test: { script: 'rspec' }, deploy: { trigger: { include: included_files } } - } + }) end let(:included_files) do @@ -112,17 +179,46 @@ describe Ci::CreatePipelineService, '#execute' do Gitlab::Ci::Config::Entry::Trigger::ComplexTrigger::SameProjectTrigger::INCLUDE_MAX_SIZE end - before do - stub_ci_pipeline_yaml_file(YAML.dump(config)) + it_behaves_like 'creation failure' do + let(:expected_error) { /trigger:include config is too long/ } end + end - it 'returns errors' do - pipeline = create_pipeline! + context 'when including configs from artifact' do + context 'when specified dependency is in the wrong order' do + let(:config) do + <<~YAML + test: + trigger: + include: + - job: generator + artifact: 'generated.yml' + generator: + stage: 'deploy' + script: 'generator' + YAML + end - expect(pipeline.errors.full_messages.first).to match(/trigger:include config is too long/) - expect(pipeline.failure_reason).to eq 'config_error' - expect(pipeline).to be_persisted - expect(pipeline.status).to eq 'failed' + it_behaves_like 'creation failure' do + let(:expected_error) { /test job: dependency generator is not defined in prior stages/ } + end + end + + context 'when specified dependency is missing :job key' do + let(:config) do + <<~YAML + test: + trigger: + include: + - artifact: 'generated.yml' + YAML + end + + it_behaves_like 'creation failure' do + let(:expected_error) do + /include config must specify the job where to fetch the artifact from/ + end + end end end end diff --git a/spec/services/ci/pipeline_processing/shared_processing_service.rb b/spec/services/ci/pipeline_processing/shared_processing_service.rb index ca003299535..ffe5eacfc48 100644 --- a/spec/services/ci/pipeline_processing/shared_processing_service.rb +++ b/spec/services/ci/pipeline_processing/shared_processing_service.rb @@ -757,73 +757,19 @@ shared_examples 'Pipeline Processing Service' do expect(builds.pending).to contain_exactly(deploy) end - context 'when feature ci_dag_support is disabled' do - before do - stub_feature_flags(ci_dag_support: false) - end - - it 'when linux:build finishes first it follows stages' do - expect(process_pipeline).to be_truthy - - expect(stages).to eq(%w(pending created created)) - expect(builds.pending).to contain_exactly(linux_build, mac_build) - - # we follow the single path of linux - linux_build.reset.success! - - expect(stages).to eq(%w(running created created)) - expect(builds.success).to contain_exactly(linux_build) - expect(builds.pending).to contain_exactly(mac_build) - - mac_build.reset.success! - - expect(stages).to eq(%w(success pending created)) - expect(builds.success).to contain_exactly(linux_build, mac_build) - expect(builds.pending).to contain_exactly( - linux_rspec, linux_rubocop, mac_rspec, mac_rubocop) - - linux_rspec.reset.success! - linux_rubocop.reset.success! - mac_rspec.reset.success! - mac_rubocop.reset.success! - - expect(stages).to eq(%w(success success pending)) - expect(builds.success).to contain_exactly( - linux_build, linux_rspec, linux_rubocop, mac_build, mac_rspec, mac_rubocop) - expect(builds.pending).to contain_exactly(deploy) - end - end - context 'when one of the jobs is run on a failure' do let!(:linux_notify) { create_build('linux:notify', stage: 'deploy', stage_idx: 2, when: 'on_failure', scheduling_type: :dag) } let!(:linux_notify_on_build) { create(:ci_build_need, build: linux_notify, name: 'linux:build') } context 'when another job in build phase fails first' do - context 'when ci_dag_support is enabled' do - it 'does skip linux:notify' do - expect(process_pipeline).to be_truthy - - mac_build.reset.drop! - linux_build.reset.success! - - expect(linux_notify.reset).to be_skipped - end - end - - context 'when ci_dag_support is disabled' do - before do - stub_feature_flags(ci_dag_support: false) - end - - it 'does run linux:notify' do - expect(process_pipeline).to be_truthy + it 'does skip linux:notify' do + expect(process_pipeline).to be_truthy - mac_build.reset.drop! - linux_build.reset.success! + mac_build.reset.drop! + linux_build.reset.success! - expect(linux_notify.reset).to be_pending - end + expect(linux_notify.reset).to be_skipped end end @@ -864,19 +810,6 @@ shared_examples 'Pipeline Processing Service' do expect(stages).to eq(%w(success success running)) expect(builds.pending).to contain_exactly(deploy) end - - context 'when ci_dag_support is disabled' do - before do - stub_feature_flags(ci_dag_support: false) - end - - it 'does run deploy_pages at the start' do - expect(process_pipeline).to be_truthy - - expect(stages).to eq(%w(pending created created)) - expect(builds.pending).to contain_exactly(linux_build, mac_build) - end - end end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index ac17919d7f0..a51e0b79075 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -394,6 +394,7 @@ describe PostReceive do it 'expires the status cache' do expect(snippet.repository).to receive(:empty?).and_return(true) expect(snippet.repository).to receive(:expire_status_cache) + expect(snippet.repository).to receive(:expire_statistics_caches) perform end |