diff options
52 files changed, 744 insertions, 296 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 6f1505b5c0d..8e612243371 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -134,6 +134,15 @@ review-stop: artifacts: paths: [] +review-cleanup-failed-deployment: + extends: review-stop + stage: prepare + when: on_success + needs: [] + allow_failure: false + script: + - delete_failed_release + .review-qa-base: extends: - .review-docker diff --git a/.rubocop.yml b/.rubocop.yml index f24cbb6ce92..73743ebf9a2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -275,3 +275,8 @@ RSpec/BeSuccessMatcher: - 'ee/spec/support/shared_examples/controllers/**/*' - 'spec/support/controllers/**/*' - 'ee/spec/support/controllers/**/*' +Scalability/FileUploads: + Enabled: true + Include: + - 'lib/api/**/*.rb' + - 'ee/lib/api/**/*.rb' diff --git a/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js b/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js new file mode 100644 index 00000000000..6a40f1cbc5e --- /dev/null +++ b/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js @@ -0,0 +1,11 @@ +export default { + data() { + return { + isCustomStageForm: false, + }; + }, + methods: { + showAddStageForm: () => {}, + hideAddStageForm: () => {}, + }, +}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue index d946594a069..63549596fac 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue @@ -23,7 +23,10 @@ export default { </script> <template> - <div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded"> + <div + :class="{ active: isActive }" + class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px" + > <slot></slot> <div v-if="canEdit" class="dropdown"> <gl-button diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index b3ae47af750..c9a6b10b2f3 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; import { GlEmptyState } from '@gitlab/ui'; import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins'; +import addStageMixin from 'ee_else_ce/analytics/cycle_analytics/mixins/add_stage_mixin'; import Flash from '../flash'; import { __ } from '~/locale'; import Translate from '../vue_shared/translate'; @@ -43,8 +44,12 @@ export default () => { DateRangeDropdown: () => import('ee_component/analytics/shared/components/date_range_dropdown.vue'), 'stage-nav-item': stageNavItem, + CustomStageForm: () => + import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'), + AddStageButton: () => + import('ee_component/analytics/cycle_analytics/components/add_stage_button.vue'), }, - mixins: [filterMixins], + mixins: [filterMixins, addStageMixin], data() { return { store: CycleAnalyticsStore, @@ -124,6 +129,7 @@ export default () => { return; } + this.hideAddStageForm(); this.isLoadingStage = true; this.store.setStageEvents([], stage); this.store.setActiveStage(stage); diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index c6cc04a139f..ce592720531 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -67,18 +67,14 @@ export default { saveAssignees() { this.loading = true; - function setLoadingFalse() { - this.loading = false; - } - this.mediator .saveAssignees(this.field) - .then(setLoadingFalse.bind(this)) .then(() => { + this.loading = false; refreshUserMergeRequestCounts(); }) .catch(() => { - setLoadingFalse(); + this.loading = false; return new Flash(__('Error occurred when saving assignees')); }); }, diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index cbe20f761ff..feb08e3acaf 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -1,7 +1,4 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '~/lib/utils/axios_utils'; export default class SidebarService { constructor(endpointMap) { @@ -18,23 +15,15 @@ export default class SidebarService { } get() { - return Vue.http.get(this.endpoint); + return axios.get(this.endpoint); } update(key, data) { - return Vue.http.put( - this.endpoint, - { - [key]: data, - }, - { - emulateJSON: true, - }, - ); + return axios.put(this.endpoint, { [key]: data }); } getProjectsAutocomplete(searchTerm) { - return Vue.http.get(this.projectsAutocompleteEndpoint, { + return axios.get(this.projectsAutocompleteEndpoint, { params: { search: searchTerm, }, @@ -42,11 +31,11 @@ export default class SidebarService { } toggleSubscription() { - return Vue.http.post(this.toggleSubscriptionEndpoint); + return axios.post(this.toggleSubscriptionEndpoint); } moveIssue(moveToProjectId) { - return Vue.http.post(this.moveIssueEndpoint, { + return axios.post(this.moveIssueEndpoint, { move_to_project_id: moveToProjectId, }); } diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 643fe6c00b6..4a7000cbbda 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -32,7 +32,10 @@ export default class SidebarMediator { // If there are no ids, that means we have to unassign (which is id = 0) // And it only accepts an array, hence [0] - return this.service.update(field, selected.length === 0 ? [0] : selected); + const assignees = selected.length === 0 ? [0] : selected; + const data = { assignee_ids: assignees }; + + return this.service.update(field, data); } setMoveToProjectId(projectId) { @@ -42,8 +45,7 @@ export default class SidebarMediator { fetch() { return this.service .get() - .then(response => response.json()) - .then(data => { + .then(({ data }) => { this.processFetchedData(data); }) .catch(() => new Flash(__('Error occurred when fetching sidebar data'))); @@ -71,23 +73,17 @@ export default class SidebarMediator { } fetchAutocompleteProjects(searchTerm) { - return this.service - .getProjectsAutocomplete(searchTerm) - .then(response => response.json()) - .then(data => { - this.store.setAutocompleteProjects(data); - return this.store.autocompleteProjects; - }); + return this.service.getProjectsAutocomplete(searchTerm).then(({ data }) => { + this.store.setAutocompleteProjects(data); + return this.store.autocompleteProjects; + }); } moveIssue() { - return this.service - .moveIssue(this.store.moveToProjectId) - .then(response => response.json()) - .then(data => { - if (window.location.pathname !== data.web_url) { - visitUrl(data.web_url); - } - }); + return this.service.moveIssue(this.store.moveToProjectId).then(({ data }) => { + if (window.location.pathname !== data.web_url) { + visitUrl(data.web_url); + } + }); } } diff --git a/app/assets/javascripts/vue_shared/plugins/global_toast.js b/app/assets/javascripts/vue_shared/plugins/global_toast.js new file mode 100644 index 00000000000..c0de1cdc615 --- /dev/null +++ b/app/assets/javascripts/vue_shared/plugins/global_toast.js @@ -0,0 +1,8 @@ +import Vue from 'vue'; +import { GlToast } from '@gitlab/ui'; + +Vue.use(GlToast); + +export default function showGlobalToast(...args) { + return Vue.toasted.show(...args); +} diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 33caac4d725..ba123ff9a67 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -67,6 +67,18 @@ max-height: calc(100vh - 100px); } + details { + margin-bottom: $gl-padding; + + summary { + margin-bottom: $gl-padding; + } + + *:first-child:not(summary) { + margin-top: $gl-padding; + } + } + // Single code lines should wrap code { font-family: $monospace-font; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index d80155a416d..e20711a193d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -41,7 +41,6 @@ width: 20%; } - .fa { color: $cycle-analytics-light-gray; @@ -146,7 +145,6 @@ .stage-nav-item { line-height: 65px; - border: 1px solid $border-color; &.active { background: $blue-50; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 3648ec5e239..d2906ce0780 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -15,3 +15,9 @@ font-size: $size; } } + +.border-width-1px { border-width: 1px; } +.border-style-dashed { border-style: dashed; } +.border-style-solid { border-style: solid; } +.border-color-blue-300 { border-color: $blue-300; } +.border-color-default { border-color: $border-color; } diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index e39d655325f..a2cf081375e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -31,15 +31,6 @@ class ApplicationSetting < ApplicationRecord serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize - self.ignored_columns += %i[ - clientside_sentry_dsn - clientside_sentry_enabled - koding_enabled - koding_url - sentry_dsn - sentry_enabled - ] - cache_markdown_field :sign_in_text cache_markdown_field :help_page_text cache_markdown_field :shared_runners_text, pipeline: :plain_markdown diff --git a/app/models/note.rb b/app/models/note.rb index 5bd3a7f969a..62b3f47fadd 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -33,8 +33,6 @@ class Note < ApplicationRecord end end - self.ignored_columns += %i[original_discussion_id] - cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true redact_field :note diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 637c017a342..bf2aec74ec8 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class NotificationSetting < ApplicationRecord - self.ignored_columns += %i[events] - enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 } default_value_for :level, NotificationSetting.levels[:global] diff --git a/app/models/repository.rb b/app/models/repository.rb index 5cb4b56a114..e5a83366776 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1134,6 +1134,10 @@ class Repository @cache ||= Gitlab::RepositoryCache.new(self) end + def redis_set_cache + @redis_set_cache ||= Gitlab::RepositorySetCache.new(self) + end + def request_store_cache @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) end diff --git a/app/models/user.rb b/app/models/user.rb index 5f109feb96a..9ca01715578 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,12 +23,6 @@ class User < ApplicationRecord DEFAULT_NOTIFICATION_LEVEL = :participating - self.ignored_columns += %i[ - authentication_token - email_provider - external_email - ] - add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 1dc538826dc..dfb0e7ed297 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -1,7 +1,7 @@ - issuable_type = issuable_sidebar[:type] - signed_in = !!issuable_sidebar.dig(:current_user, :id) -#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } } +#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}", signed_in: signed_in } } .title.hide-collapsed = _('Assignee') = icon('spinner spin') diff --git a/changelogs/unreleased/34338-details-element.yml b/changelogs/unreleased/34338-details-element.yml new file mode 100644 index 00000000000..95e50e1d4a9 --- /dev/null +++ b/changelogs/unreleased/34338-details-element.yml @@ -0,0 +1,5 @@ +--- +title: Add some padding to details markdown element +merge_request: 32716 +author: +type: fixed diff --git a/changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.yml b/changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.yml new file mode 100644 index 00000000000..a1f99833ce0 --- /dev/null +++ b/changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Mention in docs how to disable project snippets +merge_request: 32391 +author: Jacopo Beschi @jacopo-beschi +type: other diff --git a/changelogs/unreleased/remove-vue-resource-from-sidebar-service.yml b/changelogs/unreleased/remove-vue-resource-from-sidebar-service.yml new file mode 100644 index 00000000000..f86e0a4259f --- /dev/null +++ b/changelogs/unreleased/remove-vue-resource-from-sidebar-service.yml @@ -0,0 +1,5 @@ +--- +title: Remove vue resource from sidebar service +merge_request: 32400 +author: Lee Tickett +type: other diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 9f3e104bc2b..20f31ff6810 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -60,7 +60,7 @@ Sidekiq.configure_server do |config| # Sidekiq (e.g. in an initializer). ActiveRecord::Base.clear_all_connections! - Gitlab::SidekiqMonitor.instance.start if enable_sidekiq_monitor + Gitlab::SidekiqDaemon::Monitor.instance.start if enable_sidekiq_monitor end if enable_reliable_fetch? diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index c41edb5dbfc..fdafac8420e 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -270,7 +270,7 @@ is interrupted mid-execution and it is not guaranteed that proper rollback of transactions is implemented. ```ruby -Gitlab::SidekiqMonitor.cancel_job('job-id') +Gitlab::SidekiqDaemon::Monitor.cancel_job('job-id') ``` > This requires the Sidekiq to be run with `SIDEKIQ_MONITOR_WORKER=1` diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index f5bc6cbd988..211bbdc2bb9 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1327,3 +1327,6 @@ console: example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 - -> / ``` + +NOTE: **Note:** +You may need to [allow requests to the local network](../../../security/webhooks.md) for this receiver to be added. diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 58ccd8bf2ae..e0fb5c57784 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -32,6 +32,8 @@ links will be missing from the sidebar UI. You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see Issues or Merge Requests, both of which use Labels and Milestones, then you shouldn't be denied access to Labels and Milestones pages. +Project [Snippets](../../snippets.md) are enabled by default. + #### Disabling email notifications You can disable all email notifications related to the project by selecting the diff --git a/doc/user/snippets.md b/doc/user/snippets.md index 7b580a057f2..74f58a1a92b 100644 --- a/doc/user/snippets.md +++ b/doc/user/snippets.md @@ -33,6 +33,11 @@ overview that shows snippets you created and allows you to explore all snippets. If you want to discover snippets that belong to a specific project, you can navigate to the Snippets page via the left side navigation on the project page. +Project snippets are enabled and available by default, but they can +be disabled by navigating to your project's **Settings**, expanding +**Visibility, project features, permissions** and scrolling down to +**Snippets**. From there, you can toggle to disable them or select a +different visibility level from the dropdown menu. ## Snippet comments diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 51b7cf05c8f..c1e7af33235 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -38,7 +38,8 @@ module API optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' optional :tag_list, type: Array[String], desc: 'The list of tags for a project' - optional :avatar, type: File, desc: 'Avatar image for project' + # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960 + optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests' optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md" diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index 4227a106a95..40b133e8959 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -90,8 +90,11 @@ module API end params do requires :domain, type: String, desc: 'The domain' + # rubocop:disable Scalability/FileUploads + # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960 optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key + # rubocop:enable Scalability/FileUploads all_or_none_of :user_provided_certificate, :user_provided_key end post ":id/pages/domains" do @@ -111,8 +114,11 @@ module API desc 'Updates a pages domain' params do requires :domain, type: String, desc: 'The domain' + # rubocop:disable Scalability/FileUploads + # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960 optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key + # rubocop:enable Scalability/FileUploads end put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do authorize! :update_pages, user_project diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index bb1b037c08f..9b5e0727184 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -27,7 +27,8 @@ module API resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do params do requires :path, type: String, desc: 'The new project path and name' - requires :file, type: File, desc: 'The project export file to be imported' + # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960 + requires :file, type: File, desc: 'The project export file to be imported' # rubocop:disable Scalability/FileUploads optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace." optional :overwrite, type: Boolean, default: false, desc: 'If there is a project in the same namespace and with the same name overwrite it' optional :override_params, diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 3073c14b341..63bfa8db61c 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -478,7 +478,8 @@ module API desc 'Upload a file' params do - requires :file, type: File, desc: 'The file to be uploaded' + # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960 + requires :file, type: File, desc: 'The file to be uploaded' # rubocop:disable Scalability/FileUploads end post ":id/uploads" do UploadService.new(user_project, params[:file]).execute.to_h diff --git a/lib/api/users.rb b/lib/api/users.rb index a4ac5b629b8..99295888c8c 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -50,7 +50,8 @@ module API optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' - optional :avatar, type: File, desc: 'Avatar image for user' + # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960 + optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads optional :private_profile, type: Boolean, default: false, desc: 'Flag indicating the user has a private profile' all_or_none_of :extern_uid, :provider diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb index e40c366ed02..b2dc92ce010 100644 --- a/lib/gitlab/repository_cache_adapter.rb +++ b/lib/gitlab/repository_cache_adapter.rb @@ -23,6 +23,49 @@ module Gitlab end end + # Caches and strongly memoizes the method as a Redis Set. + # + # This only works for methods that do not take any arguments. The method + # should return an Array of Strings to be cached. + # + # In addition to overriding the named method, a "name_include?" method is + # defined. This uses the "SISMEMBER" query to efficiently check membership + # without needing to load the entire set into memory. + # + # name - The name of the method to be cached. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. + # + # It is not safe to use this method prior to the release of 12.3, since + # 12.2 does not correctly invalidate the redis set cache value. A mixed + # code environment containing both 12.2 and 12.3 nodes breaks, while a + # mixed code environment containing both 12.3 and 12.4 nodes will work. + def cache_method_as_redis_set(name, fallback: nil) + uncached_name = alias_uncached_method(name) + + define_method(name) do + cache_method_output_as_redis_set(name, fallback: fallback) do + __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend + end + end + + # Attempt to determine whether a value is in the set of values being + # cached, by performing a redis SISMEMBERS query if appropriate. + # + # If the full list is already in-memory, we're better using it directly. + # + # If the cache is not yet populated, querying it directly will give the + # wrong answer. We handle that by querying the full list - which fills + # the cache - and using it directly to answer the question. + define_method("#{name}_include?") do |value| + if strong_memoized?(name) || !redis_set_cache.exist?(name) + return __send__(name).include?(value) # rubocop:disable GitlabSecurity/PublicSend + end + + redis_set_cache.include?(name, value) + end + end + # Caches truthy values from the method. All values are strongly memoized, # and cached in RequestStore. # @@ -84,6 +127,11 @@ module Gitlab raise NotImplementedError end + # RepositorySetCache to be used. Should be overridden by the including class + def redis_set_cache + raise NotImplementedError + end + # List of cached methods. Should be overridden by the including class def cached_methods raise NotImplementedError @@ -100,6 +148,18 @@ module Gitlab end end + # Caches and strongly memoizes the supplied block as a Redis Set. The result + # will be provided as a sorted array. + # + # name - The name of the method to be cached. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. + def cache_method_output_as_redis_set(name, fallback: nil, &block) + memoize_method_output(name, fallback: fallback) do + redis_set_cache.fetch(name, &block).sort + end + end + # Caches truthy values from the supplied block. All values are strongly # memoized, and cached in RequestStore. # @@ -154,6 +214,7 @@ module Gitlab clear_memoization(memoizable_name(name)) end + expire_redis_set_method_caches(methods) expire_request_store_method_caches(methods) end @@ -169,6 +230,10 @@ module Gitlab end end + def expire_redis_set_method_caches(methods) + methods.each { |name| redis_set_cache.expire(name) } + end + # All cached repository methods depend on the existence of a Git repository, # so if the repository doesn't exist, we already know not to call it. def fallback_early?(method_name) diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb new file mode 100644 index 00000000000..6d3ac53a787 --- /dev/null +++ b/lib/gitlab/repository_set_cache.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Interface to the Redis-backed cache store for keys that use a Redis set +module Gitlab + class RepositorySetCache + attr_reader :repository, :namespace, :expires_in + + def initialize(repository, extra_namespace: nil, expires_in: 2.weeks) + @repository = repository + @namespace = "#{repository.full_path}:#{repository.project.id}" + @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace + @expires_in = expires_in + end + + def cache_key(type) + "#{type}:#{namespace}:set" + end + + def expire(key) + with { |redis| redis.del(cache_key(key)) } + end + + def exist?(key) + with { |redis| redis.exists(cache_key(key)) } + end + + def read(key) + with { |redis| redis.smembers(cache_key(key)) } + end + + def write(key, value) + full_key = cache_key(key) + + with do |redis| + redis.multi do + redis.del(full_key) + + # Splitting into groups of 1000 prevents us from creating a too-long + # Redis command + value.each_slice(1000) { |subset| redis.sadd(full_key, subset) } + + redis.expire(full_key, expires_in) + end + end + + value + end + + def fetch(key, &block) + if exist?(key) + read(key) + else + write(key, yield) + end + end + + def include?(key, value) + with { |redis| redis.sismember(cache_key(key), value) } + end + + private + + def with(&blk) + Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord + end + end +end diff --git a/lib/gitlab/sidekiq_daemon/monitor.rb b/lib/gitlab/sidekiq_daemon/monitor.rb new file mode 100644 index 00000000000..bbfca130425 --- /dev/null +++ b/lib/gitlab/sidekiq_daemon/monitor.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqDaemon + class Monitor < Daemon + include ::Gitlab::Utils::StrongMemoize + + NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications' + CANCEL_DEADLINE = 24.hours.seconds + RECONNECT_TIME = 3.seconds + + # We use exception derived from `Exception` + # to consider this as an very low-level exception + # that should not be caught by application + CancelledError = Class.new(Exception) # rubocop:disable Lint/InheritException + + attr_reader :jobs_thread + attr_reader :jobs_mutex + + def initialize + super + + @jobs_thread = {} + @jobs_mutex = Mutex.new + end + + def within_job(jid, queue) + jobs_mutex.synchronize do + jobs_thread[jid] = Thread.current + end + + if cancelled?(jid) + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'run', + queue: queue, + jid: jid, + canceled: true + ) + raise CancelledError + end + + yield + ensure + jobs_mutex.synchronize do + jobs_thread.delete(jid) + end + end + + def self.cancel_job(jid) + payload = { + action: 'cancel', + jid: jid + }.to_json + + ::Gitlab::Redis::SharedState.with do |redis| + redis.setex(cancel_job_key(jid), CANCEL_DEADLINE, 1) + redis.publish(NOTIFICATION_CHANNEL, payload) + end + end + + private + + def start_working + Sidekiq.logger.info( + class: self.class.to_s, + action: 'start', + message: 'Starting Monitor Daemon' + ) + + while enabled? + process_messages + sleep(RECONNECT_TIME) + end + + ensure + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'stop', + message: 'Stopping Monitor Daemon' + ) + end + + def stop_working + thread.raise(Interrupt) if thread.alive? + end + + def process_messages + ::Gitlab::Redis::SharedState.with do |redis| + redis.subscribe(NOTIFICATION_CHANNEL) do |on| + on.message do |channel, message| + process_message(message) + end + end + end + rescue Exception => e # rubocop:disable Lint/RescueException + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'exception', + message: e.message + ) + + # we re-raise system exceptions + raise e unless e.is_a?(StandardError) + end + + def process_message(message) + Sidekiq.logger.info( + class: self.class.to_s, + channel: NOTIFICATION_CHANNEL, + message: 'Received payload on channel', + payload: message + ) + + message = safe_parse(message) + return unless message + + case message['action'] + when 'cancel' + process_job_cancel(message['jid']) + else + # unknown message + end + end + + def safe_parse(message) + JSON.parse(message) + rescue JSON::ParserError + end + + def process_job_cancel(jid) + return unless jid + + # try to find thread without lock + return unless find_thread_unsafe(jid) + + Thread.new do + # try to find a thread, but with guaranteed + # that handle for thread corresponds to actually + # running job + find_thread_with_lock(jid) do |thread| + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'cancel', + message: 'Canceling thread with CancelledError', + jid: jid, + thread_id: thread.object_id + ) + + thread&.raise(CancelledError) + end + end + end + + # This method needs to be thread-safe + # This is why it passes thread in block, + # to ensure that we do process this thread + def find_thread_unsafe(jid) + jobs_thread[jid] + end + + def find_thread_with_lock(jid) + # don't try to lock if we cannot find the thread + return unless find_thread_unsafe(jid) + + jobs_mutex.synchronize do + find_thread_unsafe(jid).tap do |thread| + yield(thread) if thread + end + end + end + + def cancelled?(jid) + ::Gitlab::Redis::SharedState.with do |redis| + redis.exists(self.class.cancel_job_key(jid)) + end + end + + def self.cancel_job_key(jid) + "sidekiq:cancel:#{jid}" + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/monitor.rb b/lib/gitlab/sidekiq_middleware/monitor.rb index 53a6132edac..00965bf5506 100644 --- a/lib/gitlab/sidekiq_middleware/monitor.rb +++ b/lib/gitlab/sidekiq_middleware/monitor.rb @@ -4,10 +4,10 @@ module Gitlab module SidekiqMiddleware class Monitor def call(worker, job, queue) - Gitlab::SidekiqMonitor.instance.within_job(job['jid'], queue) do + Gitlab::SidekiqDaemon::Monitor.instance.within_job(job['jid'], queue) do yield end - rescue Gitlab::SidekiqMonitor::CancelledError + rescue Gitlab::SidekiqDaemon::Monitor::CancelledError # push job to DeadSet payload = ::Sidekiq.dump_json(job) ::Sidekiq::DeadSet.new.kill(payload, notify_failure: false) diff --git a/lib/gitlab/sidekiq_monitor.rb b/lib/gitlab/sidekiq_monitor.rb deleted file mode 100644 index a58b33534bf..00000000000 --- a/lib/gitlab/sidekiq_monitor.rb +++ /dev/null @@ -1,182 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - class SidekiqMonitor < Daemon - include ::Gitlab::Utils::StrongMemoize - - NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications' - CANCEL_DEADLINE = 24.hours.seconds - RECONNECT_TIME = 3.seconds - - # We use exception derived from `Exception` - # to consider this as an very low-level exception - # that should not be caught by application - CancelledError = Class.new(Exception) # rubocop:disable Lint/InheritException - - attr_reader :jobs_thread - attr_reader :jobs_mutex - - def initialize - super - - @jobs_thread = {} - @jobs_mutex = Mutex.new - end - - def within_job(jid, queue) - jobs_mutex.synchronize do - jobs_thread[jid] = Thread.current - end - - if cancelled?(jid) - Sidekiq.logger.warn( - class: self.class.to_s, - action: 'run', - queue: queue, - jid: jid, - canceled: true - ) - raise CancelledError - end - - yield - ensure - jobs_mutex.synchronize do - jobs_thread.delete(jid) - end - end - - def self.cancel_job(jid) - payload = { - action: 'cancel', - jid: jid - }.to_json - - ::Gitlab::Redis::SharedState.with do |redis| - redis.setex(cancel_job_key(jid), CANCEL_DEADLINE, 1) - redis.publish(NOTIFICATION_CHANNEL, payload) - end - end - - private - - def start_working - Sidekiq.logger.info( - class: self.class.to_s, - action: 'start', - message: 'Starting Monitor Daemon' - ) - - while enabled? - process_messages - sleep(RECONNECT_TIME) - end - - ensure - Sidekiq.logger.warn( - class: self.class.to_s, - action: 'stop', - message: 'Stopping Monitor Daemon' - ) - end - - def stop_working - thread.raise(Interrupt) if thread.alive? - end - - def process_messages - ::Gitlab::Redis::SharedState.with do |redis| - redis.subscribe(NOTIFICATION_CHANNEL) do |on| - on.message do |channel, message| - process_message(message) - end - end - end - rescue Exception => e # rubocop:disable Lint/RescueException - Sidekiq.logger.warn( - class: self.class.to_s, - action: 'exception', - message: e.message - ) - - # we re-raise system exceptions - raise e unless e.is_a?(StandardError) - end - - def process_message(message) - Sidekiq.logger.info( - class: self.class.to_s, - channel: NOTIFICATION_CHANNEL, - message: 'Received payload on channel', - payload: message - ) - - message = safe_parse(message) - return unless message - - case message['action'] - when 'cancel' - process_job_cancel(message['jid']) - else - # unknown message - end - end - - def safe_parse(message) - JSON.parse(message) - rescue JSON::ParserError - end - - def process_job_cancel(jid) - return unless jid - - # try to find thread without lock - return unless find_thread_unsafe(jid) - - Thread.new do - # try to find a thread, but with guaranteed - # that handle for thread corresponds to actually - # running job - find_thread_with_lock(jid) do |thread| - Sidekiq.logger.warn( - class: self.class.to_s, - action: 'cancel', - message: 'Canceling thread with CancelledError', - jid: jid, - thread_id: thread.object_id - ) - - thread&.raise(CancelledError) - end - end - end - - # This method needs to be thread-safe - # This is why it passes thread in block, - # to ensure that we do process this thread - def find_thread_unsafe(jid) - jobs_thread[jid] - end - - def find_thread_with_lock(jid) - # don't try to lock if we cannot find the thread - return unless find_thread_unsafe(jid) - - jobs_mutex.synchronize do - find_thread_unsafe(jid).tap do |thread| - yield(thread) if thread - end - end - end - - def cancelled?(jid) - ::Gitlab::Redis::SharedState.with do |redis| - redis.exists(self.class.cancel_job_key(jid)) - end - end - - def self.cancel_job_key(jid) - "sidekiq:cancel:#{jid}" - end - end -end diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb index 3021a91dd83..483bfe12c68 100644 --- a/lib/gitlab/utils/strong_memoize.rb +++ b/lib/gitlab/utils/strong_memoize.rb @@ -24,13 +24,17 @@ module Gitlab # end # def strong_memoize(name) - if instance_variable_defined?(ivar(name)) + if strong_memoized?(name) instance_variable_get(ivar(name)) else instance_variable_set(ivar(name), yield) end end + def strong_memoized?(name) + instance_variable_defined?(ivar(name)) + end + def clear_memoization(name) remove_instance_variable(ivar(name)) if instance_variable_defined?(ivar(name)) end diff --git a/rubocop/cop/scalability/file_uploads.rb b/rubocop/cop/scalability/file_uploads.rb new file mode 100644 index 00000000000..83017217e32 --- /dev/null +++ b/rubocop/cop/scalability/file_uploads.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Scalability + # This cop checks for `File` params in API + # + # @example + # + # # bad + # params do + # requires :file, type: File + # end + # + # params do + # optional :file, type: File + # end + # + # # good + # params do + # requires :file, type: ::API::Validations::Types::WorkhorseFile + # end + # + # params do + # optional :file, type: ::API::Validations::Types::WorkhorseFile + # end + # + class FileUploads < RuboCop::Cop::Cop + MSG = 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' + + def_node_search :file_type_params?, <<~PATTERN + (send nil? {:requires :optional} (sym _) (hash <(pair (sym :type)(const nil? :File)) ...>)) + PATTERN + + def_node_search :file_types_params?, <<~PATTERN + (send nil? {:requires :optional} (sym _) (hash <(pair (sym :types)(array <(const nil? :File) ...>)) ...>)) + PATTERN + + def be_file_param_usage?(node) + file_type_params?(node) || file_types_params?(node) + end + + def on_send(node) + return unless be_file_param_usage?(node) + + add_offense(find_file_param(node), location: :expression) + end + + private + + def find_file_param(node) + node.each_descendant.find { |children| file_node_pattern.match(children) } + end + + def file_node_pattern + @file_node_pattern ||= RuboCop::NodePattern.new("(const nil? :File)") + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index c342df6d6c9..9d97aa86bf6 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -37,6 +37,7 @@ require_relative 'cop/rspec/factories_in_migration_specs' require_relative 'cop/rspec/top_level_describe_path' require_relative 'cop/qa/element_with_pattern' require_relative 'cop/sidekiq_options_queue' +require_relative 'cop/scalability/file_uploads' require_relative 'cop/destroy_all' require_relative 'cop/ruby_interpolation_in_translation' require_relative 'code_reuse_helpers' diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index e82b83a1e59..76eb67b1a2e 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -49,6 +49,26 @@ function delete_release() { helm delete --purge "$name" } +function delete_failed_release() { + if [ -z "$CI_ENVIRONMENT_SLUG" ]; then + echoerr "No release given, aborting the delete!" + return + fi + + if ! deploy_exists "${KUBE_NAMESPACE}" "${CI_ENVIRONMENT_SLUG}"; then + echoinfo "No Review App with ${CI_ENVIRONMENT_SLUG} is currently deployed." + else + # Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade` + if previous_deploy_failed "$CI_ENVIRONMENT_SLUG" ; then + echoinfo "Review App deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG" + delete_release + else + echoinfo "Review App deployment in good state" + fi + fi +} + + function get_pod() { local app_name="${1}" local status="${2-Running}" @@ -193,7 +213,6 @@ function deploy() { HELM_CMD=$(cat << EOF helm upgrade --install \ - --force \ --wait \ --timeout 900 \ --set ci.branch="$CI_COMMIT_REF_NAME" \ diff --git a/spec/frontend/vue_shared/plugins/global_toast_spec.js b/spec/frontend/vue_shared/plugins/global_toast_spec.js new file mode 100644 index 00000000000..551abe3cb41 --- /dev/null +++ b/spec/frontend/vue_shared/plugins/global_toast_spec.js @@ -0,0 +1,24 @@ +import toast from '~/vue_shared/plugins/global_toast'; +import Vue from 'vue'; + +describe('Global toast', () => { + let spyFunc; + + beforeEach(() => { + spyFunc = jest.spyOn(Vue.toasted, 'show').mockImplementation(() => {}); + }); + + afterEach(() => { + spyFunc.mockRestore(); + }); + + it('should pass all args to Vue toasted', () => { + const arg1 = 'TestMessage'; + const arg2 = { className: 'foo' }; + + toast(arg1, arg2); + + expect(Vue.toasted.show).toHaveBeenCalledTimes(1); + expect(Vue.toasted.show).toHaveBeenCalledWith(arg1, arg2); + }); +}); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 7f20b0da991..3ee97b978fd 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -210,14 +210,4 @@ const mockData = { }, }; -mockData.sidebarMockInterceptor = function(request, next) { - const body = this.responseMap[request.method.toUpperCase()][request.url]; - - next( - request.respondWith(JSON.stringify(body), { - status: 200, - }), - ); -}.bind(mockData); - export default mockData; diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index 016f5e033a5..e808f4003ff 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -1,4 +1,3 @@ -import _ from 'underscore'; import Vue from 'vue'; import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue'; import SidebarMediator from '~/sidebar/sidebar_mediator'; @@ -14,8 +13,6 @@ describe('sidebar assignees', () => { preloadFixtures('issues/open-issue.html'); beforeEach(() => { - Vue.http.interceptors.push(Mock.sidebarMockInterceptor); - loadFixtures('issues/open-issue.html'); mediator = new SidebarMediator(Mock.mediator); @@ -38,7 +35,6 @@ describe('sidebar assignees', () => { SidebarService.singleton = null; SidebarStore.singleton = null; SidebarMediator.singleton = null; - Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor); }); it('calls the mediator when saves the assignees', () => { diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 6c69c08e733..b0412105e3f 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,31 +1,37 @@ -import _ from 'underscore'; -import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarService from '~/sidebar/services/sidebar_service'; import Mock from './mock_data'; +const { mediator: mediatorMockData } = Mock; + describe('Sidebar mediator', function() { + let mock; + beforeEach(() => { - Vue.http.interceptors.push(Mock.sidebarMockInterceptor); - this.mediator = new SidebarMediator(Mock.mediator); + mock = new MockAdapter(axios); + + this.mediator = new SidebarMediator(mediatorMockData); }); afterEach(() => { SidebarService.singleton = null; SidebarStore.singleton = null; SidebarMediator.singleton = null; - Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor); + mock.restore(); }); it('assigns yourself ', () => { this.mediator.assignYourself(); - expect(this.mediator.store.currentUser).toEqual(Mock.mediator.currentUser); - expect(this.mediator.store.assignees[0]).toEqual(Mock.mediator.currentUser); + expect(this.mediator.store.currentUser).toEqual(mediatorMockData.currentUser); + expect(this.mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser); }); it('saves assignees', done => { + mock.onPut(mediatorMockData.endpoint).reply(200, {}); this.mediator .saveAssignees('issue[assignee_ids]') .then(resp => { @@ -36,8 +42,8 @@ describe('Sidebar mediator', function() { }); it('fetches the data', done => { - const mockData = - Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras']; + const mockData = Mock.responseMap.GET[mediatorMockData.endpoint]; + mock.onGet(mediatorMockData.endpoint).reply(200, mockData); spyOn(this.mediator, 'processFetchedData').and.callThrough(); this.mediator @@ -50,8 +56,7 @@ describe('Sidebar mediator', function() { }); it('processes fetched data', () => { - const mockData = - Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras']; + const mockData = Mock.responseMap.GET[mediatorMockData.endpoint]; this.mediator.processFetchedData(mockData); expect(this.mediator.store.assignees).toEqual(mockData.assignees); @@ -74,6 +79,7 @@ describe('Sidebar mediator', function() { it('fetches autocomplete projects', done => { const searchTerm = 'foo'; + mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {}); spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough(); spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough(); @@ -88,7 +94,9 @@ describe('Sidebar mediator', function() { }); it('moves issue', done => { + const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint]; const moveToProjectId = 7; + mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData); this.mediator.store.setMoveToProjectId(moveToProjectId); spyOn(this.mediator.service, 'moveIssue').and.callThrough(); const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl'); @@ -97,7 +105,7 @@ describe('Sidebar mediator', function() { .moveIssue() .then(() => { expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId); - expect(visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); + expect(visitUrl).toHaveBeenCalledWith(mockData.web_url); }) .then(done) .catch(done.fail); @@ -105,6 +113,7 @@ describe('Sidebar mediator', function() { it('toggle subscription', done => { this.mediator.store.setSubscribedState(false); + mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {}); spyOn(this.mediator.service, 'toggleSubscription').and.callThrough(); this.mediator diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 230e0a933a9..ec712450f2e 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -import _ from 'underscore'; -import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarService from '~/sidebar/services/sidebar_service'; @@ -8,8 +8,12 @@ import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue'; import Mock from './mock_data'; describe('SidebarMoveIssue', function() { + let mock; + beforeEach(() => { - Vue.http.interceptors.push(Mock.sidebarMockInterceptor); + mock = new MockAdapter(axios); + const mockData = Mock.responseMap.GET['/autocomplete/projects?project_id=15']; + mock.onGet('/autocomplete/projects?project_id=15').reply(200, mockData); this.mediator = new SidebarMediator(Mock.mediator); this.$content = $(` <div class="dropdown"> @@ -37,8 +41,7 @@ describe('SidebarMoveIssue', function() { SidebarMediator.singleton = null; this.sidebarMoveIssue.destroy(); - - Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor); + mock.restore(); }); describe('init', () => { diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index 808eb865a21..fd1338b55a6 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -6,6 +6,7 @@ describe Gitlab::RepositoryCacheAdapter do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:cache) { repository.send(:cache) } + let(:redis_set_cache) { repository.send(:redis_set_cache) } describe '#cache_method_output', :use_clean_rails_memory_store_caching do let(:fallback) { 10 } @@ -208,9 +209,11 @@ describe Gitlab::RepositoryCacheAdapter do describe '#expire_method_caches' do it 'expires the caches of the given methods' do expect(cache).to receive(:expire).with(:rendered_readme) - expect(cache).to receive(:expire).with(:gitignore) + expect(cache).to receive(:expire).with(:branch_names) + expect(redis_set_cache).to receive(:expire).with(:rendered_readme) + expect(redis_set_cache).to receive(:expire).with(:branch_names) - repository.expire_method_caches(%i(rendered_readme gitignore)) + repository.expire_method_caches(%i(rendered_readme branch_names)) end it 'does not expire caches for non-existent methods' do diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb new file mode 100644 index 00000000000..87e51f801e5 --- /dev/null +++ b/spec/lib/gitlab/repository_set_cache_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:namespace) { "#{repository.full_path}:#{project.id}" } + let(:cache) { described_class.new(repository) } + + describe '#cache_key' do + subject { cache.cache_key(:foo) } + + it 'includes the namespace' do + is_expected.to eq("foo:#{namespace}:set") + end + + context 'with a given namespace' do + let(:extra_namespace) { 'my:data' } + let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) } + + it 'includes the full namespace' do + is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set") + end + end + end + + describe '#expire' do + it 'expires the given key from the cache' do + cache.write(:foo, ['value']) + + expect(cache.read(:foo)).to contain_exactly('value') + expect(cache.expire(:foo)).to eq(1) + expect(cache.read(:foo)).to be_empty + end + end + + describe '#exist?' do + it 'checks whether the key exists' do + expect(cache.exist?(:foo)).to be(false) + + cache.write(:foo, ['value']) + + expect(cache.exist?(:foo)).to be(true) + end + end + + describe '#fetch' do + let(:blk) { -> { ['block value'] } } + + subject { cache.fetch(:foo, &blk) } + + it 'fetches the key from the cache when filled' do + cache.write(:foo, ['value']) + + is_expected.to contain_exactly('value') + end + + it 'writes the value of the provided block when empty' do + cache.expire(:foo) + + is_expected.to contain_exactly('block value') + expect(cache.read(:foo)).to contain_exactly('block value') + end + end + + describe '#include?' do + it 'checks inclusion in the Redis set' do + cache.write(:foo, ['value']) + + expect(cache.include?(:foo, 'value')).to be(true) + expect(cache.include?(:foo, 'bar')).to be(false) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_monitor_spec.rb b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb index bbd7bf90217..acbb09e3542 100644 --- a/spec/lib/gitlab/sidekiq_monitor_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::SidekiqMonitor do +describe Gitlab::SidekiqDaemon::Monitor do let(:monitor) { described_class.new } describe '#within_job' do @@ -43,7 +43,7 @@ describe Gitlab::SidekiqMonitor do before do # we want to run at most once cycle # we toggle `enabled?` flag after the first call - stub_const('Gitlab::SidekiqMonitor::RECONNECT_TIME', 0) + stub_const('Gitlab::SidekiqDaemon::Monitor::RECONNECT_TIME', 0) allow(monitor).to receive(:enabled?).and_return(true, false) allow(Sidekiq.logger).to receive(:info) diff --git a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb index 7319cdc2399..023df1a6391 100644 --- a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb @@ -10,8 +10,8 @@ describe Gitlab::SidekiqMiddleware::Monitor do let(:job) { { 'jid' => 'job-id' } } let(:queue) { 'my-queue' } - it 'calls SidekiqMonitor' do - expect(Gitlab::SidekiqMonitor.instance).to receive(:within_job) + it 'calls Gitlab::SidekiqDaemon::Monitor' do + expect(Gitlab::SidekiqDaemon::Monitor.instance).to receive(:within_job) .with('job-id', 'my-queue') .and_call_original @@ -29,7 +29,7 @@ describe Gitlab::SidekiqMiddleware::Monitor do context 'when cancel happens' do subject do monitor.call(worker, job, queue) do - raise Gitlab::SidekiqMonitor::CancelledError + raise Gitlab::SidekiqDaemon::Monitor::CancelledError end end diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb index 26baaf873a8..624e799c5e9 100644 --- a/spec/lib/gitlab/utils/strong_memoize_spec.rb +++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb @@ -52,6 +52,22 @@ describe Gitlab::Utils::StrongMemoize do end end + describe '#strong_memoized?' do + let(:value) { :anything } + + subject { object.strong_memoized?(:method_name) } + + it 'returns false if the value is uncached' do + is_expected.to be(false) + end + + it 'returns true if the value is cached' do + object.method_name + + is_expected.to be(true) + end + end + describe '#clear_memoization' do let(:value) { 'mepmep' } diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb new file mode 100644 index 00000000000..2a94fde5ba2 --- /dev/null +++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rubocop' +require_relative '../../../support/helpers/expect_offense' +require_relative '../../../../rubocop/cop/scalability/file_uploads' + +describe RuboCop::Cop::Scalability::FileUploads do + include CopHelper + include ExpectOffense + + subject(:cop) { described_class.new } + let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' } + + context 'with required params' do + it 'detects File in types array' do + expect_offense(<<~PATTERN.strip_indent) + params do + requires :certificate, allow_blank: false, types: [String, File] + ^^^^ #{message} + end + PATTERN + end + + it 'detects File as type argument' do + expect_offense(<<~PATTERN.strip_indent) + params do + requires :attachment, type: File + ^^^^ #{message} + end + PATTERN + end + end + + context 'with optional params' do + it 'detects File in types array' do + expect_offense(<<~PATTERN.strip_indent) + params do + optional :certificate, allow_blank: false, types: [String, File] + ^^^^ #{message} + end + PATTERN + end + + it 'detects File as type argument' do + expect_offense(<<~PATTERN.strip_indent) + params do + optional :attachment, type: File + ^^^^ #{message} + end + PATTERN + end + end +end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index fe7c6fe4700..281c7438eee 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -40,7 +40,7 @@ describe Ci::RetryBuildService do user_id auto_canceled_by_id retried failure_reason sourced_pipelines artifacts_file_store artifacts_metadata_store metadata runner_session trace_chunks upstream_pipeline_id - artifacts_file artifacts_metadata artifacts_size].freeze + artifacts_file artifacts_metadata artifacts_size commands].freeze shared_examples 'build duplication' do let(:another_pipeline) { create(:ci_empty_pipeline, project: project) } |